Search

Untrustedlife

Your window into the mind of a questionably sane queer non-binary indie horror game dev

Author

untrustedlife

Hey there. I am Untrustedlife. I develop indie games in my free time. For more info check out my About Page. https://untrustedlife.com/about/

Pilgrimage: Version 0.3.2

Okay, so, apparently I made 113 commits since the last version update. I thought I had mostly been messing with balance and infrastructure, but that’s clearly nonsense because there is actually quite a bit here O_O

  • Shiny creatures now exist, they hav eteh same base odds as Pokemon 1 in 4096
  • Shinies recieve an exploding 1d6 bonus to Tenacity, so a lucky shiny can become completely ridiculous and overpowered
  • Shiny creatures appear in Tenacity Yellow in encounters.
  • They even get unique birth events
  • Species now define their own default starting skills in their block files
  • Those skills are applied when an individual creature is created, including the player.
  • Players cannot remove or refund the natural skill levels belonging to their species during character creation.
  • Added these starting skills to nearly the entire creature roster
  • Species should all feel a bit more differnet from each other without me having to add whole new mechanics (yet)
  • Also, leveling costs are based on how many levels you’ve gained, not your starting skill. So if you naturally start with Fighting 2, getting to Fighting 3 still only costs 50 XP, as though level 3 were your first level.
  • Swimming now exists as a skill
  • Creatures can clumsily fall into water in the history sim, and if that happens teh cretaure must mak e aswimming roll to avoid drowning
  • One out of every 6 drowning checks is now substantially harder
  • Also added a bunch of fun event texts for it, including creatures sinking like bricks and ending up in Davy Jones’ locker.
  • This means aquatic creatures and natural swimmers are now distinguished mechaniclaly , swimming is also now used in a bunch of other checks
  • Replaced the old binary aquatic plant concept with an actual habitat system. (Yeah for nonbinary concepts :3)
  • Plants can now be aquatic, terrestrial, or amphibious.
  • Land and water encounters now pull from the appropriate plant pools.
  • Amphibious plants can appear in aquatic gameplay when appropriate.
  • The bestiary also now displays a plant’s habitat.
  • Added seven terrestrial plants:
    • Beach grass
    • Bittersweet nightshade
    • Maneater
    • Moss
    • Nightshade
    • River willow
    • Venus flytrap
  • Different regions can now have their own associated terrestrial plants.
  • Historical figures encounter plants much more frequently now that plant encounters are substantially more survivable.
  • Expanded the procedural plant-name generator by a dumbstupid amount.
  • Suffocating plant generation now chooses its associated spheres more intelligently.
  • Historical creatures can now roll to avoid dying from natural causes.
  • They can escape suffocating plants via rolling foraging tenacity, escape artist, etc.
  • They can overcome starvation through Foraging skill checks.
  • They can survive poisonous plants. (Just like the player)
  • They can fall into water, attempt to swim, and either survive or drown or get killed by some random eldritch plant that lives in the water in that region.
  • Successful escapes and survival rolls now produce their own history events instead of simply causing nothing to happen (And this applys in game too, not just in the sim, so if you starve then get out fo starvation, it is logged as a historical event).
  • Fixed encountered creatures not receiving XP or leveling correctly.
  • Cleaned up my dice roll system so the same systems can work for player-versus-creature, creature-versus-creature, and world-simulation events instead of assuming every roll involves the player.
  • Massively increased the variety of pretty much all historical events
  • Combat descriptions now use combanatorial sentance generation, which produces an absurd level of variation
  • Added more mundane combat descriptions so not every fight is a elaborate longwinded paragrpah about the metaphysical concept of tenacity or whatever
  • Added a system for less verbose combat text (which isn’t used for players yet, but is stored on events)
  • Fixed several grammar, comma, article, repetition, and event-ordering problems.
  • Added the Silkfolk
    • They are an extremely long-lived sapient species whose long lives encourage insane skill growth. (I wanted my freiren style  depressed, bored,  elves who master everything gioven enough time) 
  • Added the “Tenacious Ones” as a kind of distorted mirror to the Silkfolk.
    • Tenacious Ones are ancient, long lived, bloodless shapeshifters who lack empathy and patience but can relentlessly pursue a goal until they master it, which, with 1000 yea rlives can get quite insurmountable.
  • Added more detail and mechanical identities to the existing sapient species.
  • Gods now start with extremely powerful skills, but not such ridiculous ones that mortals have absolutely no hope of killing them.
  • Added additional possible historical names, including Mara, Ringo, Dante, and Nibbles (hehe).
  • Added the first version of my probability-collapse/wavefunction collapse resolver system.
  • The resolver can take weighted possibilities, collapse them into a concrete fact, apply already-established truths, and then permanently materialize the result into the world save.
  • Creature creation is now using this shared resolution flow.
  • Shininess is the first real application of it, but the system is intended to eventually handle unresolved historical and world details without contradicting things the game has already established, so i can do more on the fly procedural genration when you interact with things (The world state need not be fully decided on world gen), this will also allow me to optimize save file size and do crazy detaield pot-hoc world/history generation.
  • Added an initial truth/constraint system, which right now supports facts such as a creature already being known to have been born on a particular date, before they are generated.
  • Consolidated several previously separate creature-generation paths.
  • Simplified plant, region, and block getters and shit ton of duplicate code.
  • Removed redundant hardcoded nonsense
  • Added a verbosity-controlled debug logger and converted a number of miscellaneous debug prints to use it.
  • More spots use the cleaner tooltip style that was previously only used in Kings Sea III mode
  • Added tooltips explaining the available game modes.
  • Added King’s Wilds to the interaction screen as a disabled upcoming mode.
  • Added initial support for POIs
  • Fixed several character reroll and skill-reset issues.
  • Reduced noisy debug output and added a controllable verbosity system to the game.
  • Fixed the Canon Viewer producing noisy errors while loading.
  • Fixed aquatic and terrestrial plant lists being mixed up.
  • Fixed several plant retrieval and habitat-selection bugs.
  • Fixed historical figures failing to level after encounters.
  • Fixed assorted commas, duplicate phrases, bookkeeping errors, reversed conditions, and other little acts of violence I had committed against the codebase.
  • Fixed indefinite articles around words such as “one.”
  • Fixed several newly added survival events using species names instead of individual names.
  • Various bug fixes i probably forgot

So, this update adds shinies, species-specific starting skills, swimming and drowning, plant habitats, terrestrial plants, two new sapient species, more fleshed out creatures, new historical events that can also trigger on the player, a vastly expanded text generator, and the beginning of an entire wavefunction-collapse system.

That last bit is especially exciting to me. Because instead of generating every single detail about the world upfront, the game can now leave something unresolved in a superstate until it actually becomes relevant to the player or simulation. It can then collapse the available possibilities into one permanent answer while respecting everything history has already established. 

Right now, shininess/creatures are the first real use of this system, but eventually it could determine things like what is inside a cave, whether a town built a statue of a historical figure, or other details that nobody had any reason to know until the player or sim encountered them. The world can essentially fill itself in as it is observed without contradicting its own past. Which is extremely cool. I think :3

I will say, a lot of this stuff is not very up front to the player. So playing now won’t feel substantially differnet from playing it before this release, unless you not being able to throw well as a tenacity jelly was bothering you :3. There is just more, more stuff happening beneath the surface, more detailed simulation and better text, and occasional new rare cool things and more cretaures. 

Pilgrimage: Version 0.3.1

Version 0.3.1

  • Historical figures now encounter actual plants from their region during the life simulation, with plants selected according to their frequency. 
    • Plant encounters can now kill historical figures in specific ways, including suffocation, poisoning, or starvation after failing to consume hard food. Death canon events record the specific plant involved.
  • Historical figures now use the same exact plant encounter rolls as players (I plan to keep it this way for all rolls ever) , including Foraging, Escape Artist, Tenacity, Inquiry, plant hardness, and suffocation difficulty. Simulation rolls use normal XP gain while player rolls retain player-scaled XP progression.
  • Hunger is no longer simply an automatic death when the hunger check triggers. Creatures now get a Foraging crisis roll, with broader diets making starvation easier to survive.
  • Sapient historical figures are much less likely to randomly initiate combat, while legendary and territorially dominant sapient figures remain more aggressive.
  • Dramatically expanded procedural historical-figure naming, adding a large pool of first names, titles, and many new combinations such as named heroes, villains, peasants, fish, and assorted increasingly ridiculous titles.
  • Species names can now be dynamically reformatted when used inside generated names, including spaces, hyphens, and their original form, allowing more natural name combinations.
  • Added “View Canondex” as another possible Bestiary label.
  • Updated UI colors, including lavender button text and gold hover text, and cleaned up some inconsistent button coloring.
  • Fixed a fisherman encounter bug where bait could remain invisible.
  • Various minor simulation/cache fixes, including correctly caching frequency-weighted plant lists and properly tracking plant-caused deaths within the current simulation tick.
  • The main menu version/copyright text now automatically uses the actual project version instead of a hardcoded version number.

  • My Raycasting Adventure

    Hey folks!

    It’s been years since my last post on this site (game lists/downloads have stayed current though). I’ve built several new games since then, and now I’m back to talk raycasting. (Bella is doing great btw heh, picture of her below :D) Shes such a sweety.

    A relaxed dog lying on a blanket, illuminated by pink light.

    Over the past few months I’ve been building a raycasting engine from scratch in javascript. I didn’t take many screenshots along the way, so this isn’t a blow-by-blow devlog, just a place to gather my thoughts as a sort of post-mortom. As someone who usually reaches for engines like Godot or Unity, rolling my own was surprisingly refreshing. I can shape it exactly to what I need.

    Quick refresher: raycasting, popularized by id Software’s Wolfenstein 3D, is a rendering technique where you cast a ray for each vertical column of the screen. When a ray hits a wall, you render that slice of the wall using your available data (e.g., texture/column index).

    But I go way beyond wolf3d in my renderer…

    I support multi-height walls, variable-height floors and ceilings, far-wall rendering, (Above shorter nearer walls) and a per-pixel depth buffer. Sprites can appear above distant walls, and I have ‘counters’ (walls with a walkable floor on top), ‘bumps’ (prismatic floor rises you can step onto liek counters), and ‘holes’, deeper sub-floors with proper ravine transitions, plus liquids. Think Doom-style effects, but built entirely on DDA (Doom used sectors, not DDA), and much more.

    A screenshot of a raycasting game interface showcasing a pixelated environment with a player character holding an item, a health and ammo status display, and an atmospheric fog effect. The scene features a stone wall in the background, a dark water surface, and a visible HUD with player's action prompts.

    DDA & the projection math (how this thing actually draws)

    The hardest part wasn’t “making pixels go brr”, it was comprehending the projection math. Here’s the high level mental model I landed on, with the exact variables from my renderer.

    1) From camera to ray

    For each screen column screenColumnX, I build a ray on the camera plane:

    camX = 2*(screenColumnX + 0.5)/WIDTH - 1
    rayX = dirX + planeX * camX
    rayY = dirY + planeY * camX
    

    I also keep rayDirXRecip = 1/(rayX || 1e-9) and rayDirYRecip = 1/(rayY || 1e-9) around because DDA needs reciprocals a lot.

    2) DDA = grid walking with constant cost

    DDA (Digital Differential Analyzer) is a cheap way to march the ray through a tile grid one cell at a time. I set:

    mapX = floor(player.x), mapY = floor(player.y)
    
    deltaDistanceX = |1 / rayX|
    deltaDistanceY = |1 / rayY|
    
    stepX = rayX < 0 ? -1 : 1
    stepY = rayY < 0 ? -1 : 1
    
    sideDistanceX = (stepX < 0 ? (player.x - mapX) : (mapX + 1 - player.x)) * deltaDistanceX
    sideDistanceY = (stepY < 0 ? (player.y - mapY) : (mapY + 1 - player.y)) * deltaDistanceY
    

    Then loop: pick the smaller of sideDistanceX/Y, step in that axis, add deltaDistanceX/Y, and check the cell. When I hit a non-zero MAP[y][x], I’ve hit a wall; wallSide tells me if I crossed a vertical (X) or horizontal (Y) grid line.

    This version uses Amanatides & Woo style parameters (1/|ray| strides) instead of the classic sqrt(1+(rayY/rayX)^2) form. Same result, fewer flops. More like voxels then a traditional raycaster.

    (http://www.cse.yorku.ca/~amana/research/grid.pdf)

    A pixelated first-person view of a 3D game scene showing a corridor with textured walls, a red floor, and a character holding a weapon. The top of the screen displays text indicating in-game actions and status, while the bottom shows player stats and controls.

    3) Perpendicular distance

    The DDA gives me the distance to the grid line I just crossed (sideDistanceX or sideDistanceY). To turn that into the perpendicular distance used for correct wall height I do:

    perpDist = (wallSide == 0 ? sideDistanceX - deltaDistanceX
                              : sideDistanceY - deltaDistanceY)
    perpDist = max(0.01, perpDist)
    

    Subtracting one deltaDistance* undoes the “one step too far” from earlier.

    4) Projecting world to screen (why the wall lines have the right size)

    Everything hangs on two ideas:

    • The horizon is HALF_HEIGHT. (Half the LOGICAL height, which is the height I used for the renderer, not the final output height, which is scaled up)
    • Your eye’s vertical position relative to a 2-unit wall turns into a scale:
      eyeScale = HEIGHT * (2 - floorDepth - EYE) * 0.5

    From similar triangles:

    lineHeight = (HEIGHT / perpDist) | 0
    bottomY    = (HALF_HEIGHT + eyeScale / perpDist) | 0
    topY       = bottomY - lineHeight
    

    That’s the canonical “sprite/wall projection” relationship in a raycaster, adapted for per-zone (My version of wolf3d style sectors but with variable floor and cieling heights, depths, and colors) floors (floorDepth) and player height EYE.

    Tall walls / stacked segments

    A game interface displaying a 3D scene with green walls, a blue sky, and a brown bow in the foreground. The top of the screen features health and message prompts, while the bottom shows status indicators and control instructions.

    So that walls can be taller than 1 “unit” tall. 1 unit in my raycaster is 64 pixels... I split a “wall” struct into stacked slices (each one a “unit” or less on screen). For each slice I compute source srcY/srcH and draw via a function i amde called drawWallColumnImg(...). This is how I get multi-level / stacked geometry with correct UVs.

    A screenshot of a raycasting engine in action, displaying a first-person perspective in a stylized 3D environment. The scene includes green walls, a wooden crate, and a character holding a bow. In the top part of the screen, there is an information display showing text messages and the FPS count, while the bottom section features player status details.

    5) Texture addressing & side flipping

    To render tetxures, I find where the ray actually hit the wall in world space:

    hitX = player.x + perpDist * rayX
    hitY = player.y + perpDist * rayY
    

    Use the fractional coordinate perpendicular to the wall to pick the texture column:

    • If we hit a vertical wall (wallSide==0), use frac(hitY).
    • If we hit a horizontal wall, use frac(hitX).

    Flip the column for consistent facing (N/E/S/W) based on stepX/stepY, then:

    textureColumnX = (fraction * TEXTURE_HEIGHT) | 0  // square atlas columns
    

    I select the actual texture id per side via WALL_MAP[hitTextureId].textures[wallSideDirection][seg], then look up a pre-shaded variant using SHADE_LEVELS and nearestIndexInAscendingOrder.

    This keeps textures from being accidentally mirrored, so any text stays readable, by flipping UVs based on the wall face, ensuring what you see matches the source image. Because I know the hit side (N/E/S/W), I can also choose side-specific textures and even support things like portals, or rendering the “below” of walls above me. Many raycasters treat these as ‘mid textures’ drawn in the middle of a cell; I render them on grid boundaries instead.

    6) Floors & ceilings with row-distance LUTs

    Floors/ceilings are projected by rows rather than by wall hits.

    The core identity is:

    dy = (rowY - HALF_HEIGHT)                  // pixels offset from horizon
    dist(rowY) = eyeScale / dy                 // world distance at that row
    

    I precompute these in a function called rebuildRowDistLUT():

    • ROW_DIST[y] for floors (positive, below horizon)
    • CIELING_ROW_DIST[y] for ceilings (negative sign so it marches upward)
    • Per-zone overrides ROW_DIST_BY_ZONE[z][y] and CILEING_DIST_BY_ZONE[z][y] to respect different floorDepth/ceilingHeight.

    To convert those distances into world coords along the ray cheaply, I use:

    invDot = 1 / (dirX*rayX + dirY*rayY)
    wx += rayX * deltaDist * invDot
    wy += rayY * deltaDist * invDot
    

    That keeps the floors/ceilings perfectly aligned with walls at any height without doing fresh divisions per row.

    I render them by stepping along the ray (ray-march style), which keeps the math aligned with walls and the code easy to reason about. I could halve draw calls by skipping strict per-column rendering, but I chose clarity. Performance is still strong because an x/y z-buffer stops drawing floors/ceilings where they’d be occluded, so we skip a lot of work.

    A dimly lit corridor in a retro-style video game, featuring pixelated graphics. The screen displays game status information, including player health and ammo, alongside a character holding a bow. In the background, an ominous figure and a portal can be seen.

    speaking of which…

    7) Z-buffer & occlusion

    I keep a per-pixel depth buffer pixelHeightBuffer[y*WIDTH + x] and write the wall’s perpDist for each covered pixel. That lets me:

    • Cull ceiling rows when a nearer wall is in front (castCieling checks dist >= getPixelDepth(...)).
    • Correctly depth-sort sprites later.
    • Correctly mask sprites later.
    • Avoid overdrawing floor spans already behind a wall.

    I also do an in-column occlusion pass (preprocessOcclusion) to drop hidden wall segments before drawing.

    A video game scene showing a pixelated environment with trees, walls, and a blue sky. The user interface displays health status, objectives, and control instructions at the bottom.

    8) Fog & haze

    My raycaster has fog and atmospheric effects. Unlike most raycasters. This not only allows me to squeeze out more perf (i don’t need to render stuff beyond fog) but gives it a more modern look.

    A screenshot of a raycasting game engine displaying a low-poly corridor scene with walls, a weapon in hand, and a user interface showing status and controls.

    Fog is distance-based with a soft start:

    start = player.sightDist * FOG_START_FRAC
    end   = player.sightDist
    t = clamp((dist - start) / (end - start), 0, 1)
    

    I draw a 1-px-wide band (drawFogBand) over each wall column with globalAlpha = t * 0.85, tinted by the zone’s fogColor. There’s also a full-screen “haze” gradient for mood.

    You can find an open source version of the engine here:

    Heads-up: this open-source repo hasn’t been updated to the new renderer yet (I rewrote it this past week), so parts are messy. And it lack smuch of the new functionality (spirtes over walls, cielings over short walls in tall zones, walls etc) I’m prioritizing porting the new code over. (Edit: its now ported over)

    https://github.com/Untrustedlife/InvasionEngine

    It comes with a map editor so you can easily make new maps, and of course its designed to keep game code and engine code seperated.

    How was it?

    The early version came together quickly, only like a week; But adding tall walls , far walls over near walls, and the depth buffer was the real challenge. I’ve rewritten the renderer multiple times, and the build from this last week finally supports the flashy bits like tall sprites behind distant short walls. And zone aware cielings that render over short walls in those zones etc.

    I was able to hone my javascript skills, and especially my engine optimization skills. I felt like I was actually working within limitations, channeling my inner John Carmac, unlike with commercial game engines.

    It was food for that part of my soul deep down that wishes i was around in the early days of game development instead of living in the ashes of the devastation triple A publishers have wrought in the modern day.

    I might do this again and make my own sorta minecrafty voxel engine at some point, that would be fun, or a true sector based engine like Doom.

    Who knows really, the skys the limit!

    What Now?

    This engine is flexible enough for whatever I throw at it, and the minimal overdraw keeps performance strong, even with hundreds of sprites. Short term idea is to fix bugs, restore ceiling-over-wall occlusion, add floor-over-wall occlusion, tighten sprite masking, expand wall/floor texturing, and add proper skyboxes.

    On the project side, I’ve got a few ideas: an ‘80s-style horror game where you play the villain, and a lightweight (Or even hardcore) Arena-like RPG. A lot of this is already doable without new engine work, so I may make a new game first and iterate after.

    It all runs in the broswer, which gives me a lot of advantages over most things on itchio these days. And the fact that its my own focused engine helps too.

    I had a lot of fun working on this project and as of posting thsi i’ve even updated it more! Heh

    In the meantime, you can play Realmchild Invasion (The game featured in all the screenshots here using my new engine and updated renderer) here: untrustedlife.com/realmchildinvasion

    Up ↑