<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Crafting on despatches</title><link>https://icle.es/tags/crafting/</link><description>Recent content in Crafting on despatches</description><generator>Hugo</generator><language>en</language><lastBuildDate>Fri, 27 Jun 2025 08:29:59 +0100</lastBuildDate><atom:link href="https://icle.es/tags/crafting/index.xml" rel="self" type="application/rss+xml"/><item><title>Managing Configuration</title><link>https://icle.es/2025/06/08/managing-configuration/</link><pubDate>Sun, 08 Jun 2025 21:47:05 +0000</pubDate><guid>https://icle.es/2025/06/08/managing-configuration/</guid><description>&lt;p>Before making the game available for playtesting, I wanted the player to be able
to configure the game to some degree.&lt;/p>
&lt;p>As a starting point, my keyboard layout is &lt;code>colemak&lt;/code>, and I doubt that the
controls I use would suit the majority of players.&lt;/p>
&lt;p>I am putting off a UI based config management option down the road (did I
mention that I do not enjoy GUI work?). As such I&amp;rsquo;ve been pondering alternative
configuration options.&lt;/p></description><content:encoded><![CDATA[<p>Before making the game available for playtesting, I wanted the player to be able
to configure the game to some degree.</p>
<p>As a starting point, my keyboard layout is <code>colemak</code>, and I doubt that the
controls I use would suit the majority of players.</p>
<p>I am putting off a UI based config management option down the road (did I
mention that I do not enjoy GUI work?). As such I&rsquo;ve been pondering alternative
configuration options.</p>
<h2 id="platform-independence">Platform independence</h2>
<p>Before I even get to that, the first problem I need to solve is a way to
determine the location for the config files independent of the platform.</p>
<p>Fortunately, <a href="https://github.com/ziglibs/known-folders">known-folders</a> came to
the rescue and provided an easy to use framework that can be used to determine
the various relevant locations for multiple platforms.</p>
```zig
const known_folders = @import("known-folders");
const maybe_config = try known_folders.getPath(allocator, .roaming_configuration);
if (maybe_config) |config| {
    defer allocator.free(config);
    std.debug.print("roaming config path: {s}\n", .{config});
}
```
<h2 id="locations">Locations</h2>
<p>There are three real locations of relevance for triangle</p>
<ul>
<li>The binary / package</li>
<li>Config, technically, split into two
<ul>
<li>user (or in Windows parlance, the remote dir, and can be shared across
computers)</li>
<li>system (in windows parlance, local, and is specific to that system)</li>
</ul>
</li>
<li>Save Data</li>
</ul>
<h2 id="user--game-config-files">User &amp; Game Config Files</h2>
<p>With that sorted out, the next bit is to identify the relevant config files. I
expect that triangle will continue to use these, and will eventually just get a
UI config option as well.</p>
<h3 id="user-config">User Config</h3>
<p>There are two main bits of user configuration</p>
<ul>
<li>Preference like controls</li>
<li>System details like resolution</li>
</ul>
<p>I am currently unsure when it&rsquo;ll support system config.</p>
<h3 id="game-config">Game Config</h3>
<p>There are two bits of config that the game will store. One set of config is to
remember game choices the user has made.</p>
<h4 id="remember-player-actions">Remember Player Actions</h4>
<p>For example, it will be useful to show the user details of changes to the game
since they last played. To do this, we need to track the last set of changes
that the user saw.</p>
<p>The game will show a notice on startup about its extremely early access status,
and provide an option for the user to hide that in the future. We need to save
that somewhere too.</p>
<h4 id="telemetry">Telemetry</h4>
<p>The second bit of config is metrics. While a lot of games will simply send
telemetry information directly to the developer, player privacy is really
important to me. I recognise that I will get far less data because of this, and
that there will be a bit of survivorship bias with the data - but I feel that
privacy is more important.</p>
<p>The way I want telemetry to work is that it will all be saved in a human
readable telemetry file in the config file location.</p>
<p>The data is stored only locally, and is never automatically sent. The player is
welcome to use this data for themselves if they wish and also share at their
discretion. The information will be stored in a human readable format that
should be as easy to understand as possible - no data dumps.</p>
<p>The location will also store logs (if enabled).</p>
<h2 id="setting-config">Setting Config</h2>
<p>In terms of allowing the player to manage config, there are a couple of
challenges:</p>
<ul>
<li>Providing enough documentation that it is easy to do</li>
<li>Allowing for updates, particularly to the addition of new keys</li>
</ul>
<p>To tackle this, I am going to provide an annotated template file with all the
config options. The user can create a separate file based on this with <strong>only</strong>
the config they wish to override.</p>
<p>It will be tricky to change how particular parameters are configured. E.g. If a
single value key needs to switch to an array. I&rsquo;d be loathe to sprinkle the code
with checks for legacy keys/formats. We&rsquo;ll play it by ear.</p>
<p>I considered updating the config file automatically, but this would discard any
comments the user had added. While I could offer an option to merge changes in,
it’s not straightforward enough to implement just yet.</p>
<h2 id="format">Format</h2>
<p>I&rsquo;ve been considering <code>toml</code> and <code>yaml</code> for this, with
<a href="https://github.com/sam701/zig-toml/">zig-toml</a> and
<a href="https://github.com/kubkon/zig-yaml">zig-yaml</a> respectively.</p>
<p><code>zig-yaml</code> seems to be more active (more stars, forks, issues and pr&rsquo;s and
currently also the more recent commit).</p>
<p>I am also more familiar with and prefer yaml.</p>
<p>However, it does not currently support default values. I would like the user to
have to specify only the config they&rsquo;d like to override. <code>zig-yaml</code> currently
expects all the keys to be defined if you want to parse it into a struct.</p>
<p><a href="https://github.com/kubkon/zig-yaml/issues/85">#85</a> should bring it in, but I
<a href="https://github.com/kubkon/zig-yaml/issues/92">could not get it to work</a></p>
<p>So, I tried out <code>zig-toml</code> and the test worked the first time.</p>
<p><a href="https://github.com/drone-ah/wordsonsand/tree/main/code/zig/src/toml_with_defaults.zig">src/toml_with_defaults.zig</a></p>
```zig
const std = @import("std");

const Controls = struct {
    forward: []const u8 = "w",
    craft: []const u8 = "q",
    inventory: []const u8 = "e",
};

const User = struct {
    controls: Controls = .{},
};

test "load partial toml config" {
    const toml = @import("toml");
    const allocator = std.testing.allocator;
    var parser = toml.Parser(User).init(allocator);
    defer parser.deinit();

    const source =
        \\[controls]
        \\craft = "s"
    ;
    var result = try parser.parseString(source);
    defer result.deinit();

    const config = result.value;
    const default = User{};
    try std.testing.expectEqualStrings(default.controls.forward, config.controls.forward);
    try std.testing.expectEqualStrings("s", config.controls.craft);
}
```
<h1 id="using-the-config">Using the config</h1>
<p>The final part is to <em>use</em> the config.</p>
<h2 id="loading-config">Loading config</h2>
<p>All config is loaded at startup and attached to a <code>Config</code> struct, which is in
turn part of a <code>Context</code> struct that is passed around.</p>
<p><a href="https://github.com/drone-ah/wordsonsand/tree/main/code/zig/src/load_save_config.zig">src/load_save_config.zig</a></p>
```zig
user: User,

game_path: []const u8,
game: Game,

pub fn init(allocator: std.mem.Allocator) ConfigError!Self {
    const maybe_config = known_folders.getPath(allocator, .roaming_configuration) catch {
        return ConfigError.UnableToDetermineConfigLocation;
    };
    if (maybe_config) |config| {
        defer allocator.free(config);

        // user config path
        const full_path = std.fmt.allocPrint(allocator, "{s}/triangle/user.toml", .{config}) catch {
            std.debug.panic("oom", .{});
        };
        defer allocator.free(full_path);

        // game config path
        const game_path = std.fmt.allocPrint(allocator, "{s}/triangle/game.toml", .{config}) catch {
            std.debug.panic("oom", .{});
        };

        return .{
            .user = loadConfig(allocator, User, full_path),

            .game_path = game_path,
            .game = loadConfig(allocator, Game, game_path),
        };
    }

    return ConfigError.UnableToDetermineConfigLocation;
}

fn loadConfig(allocator: std.mem.Allocator, ConfigType: type, path: []const u8) ConfigType {
    var parser = toml.Parser(ConfigType).init(allocator);
    defer parser.deinit();

    var result = parser.parseFile(path) catch {
        log.warn("unable to read config file: {s}", .{path});
        return .{};
    };
    defer result.deinit();

    return result.value;
}
```
<h2 id="user-config-controls">User config (controls)</h2>
<p>This one involves a little translation as we need to know the <code>rl.KeyboardKey</code>
for each mapping to be able to detect it.</p>
<p>I use a <code>std.StringArrayHashMapUnmanaged(rl.KeyboardKey)</code> to map the string to
each key</p>
<p><a href="https://github.com/drone-ah/wordsonsand/tree/main/code/zig/src/load_save_config.zig">src/load_save_config.zig</a></p>
```zig
const Input = struct {
    keymap: KeyMaps,

    pub fn init(allocator: std.mem.Allocator) Self {
        var keymap = KeyMaps{};
        for (default_keybindings) |entry| {
            keymap.put(allocator, entry.name, entry.key) catch {
                std.debug.panic("oom", .{});
            };
        }

        return .{
            .keymap = keymap,
        };
    }

    const KeyMap = struct {
        name: []const u8,
        key: rl.KeyboardKey,
    };

    const default_keybindings = [_]KeyMap{
        .{ .name = "a", .key = .a },
        .{ .name = "b", .key = .b },
        .{ .name = "c", .key = .c },
    };
}
```
<h2 id="game-config-1">Game Config</h2>
<p>As a starting point, we&rsquo;ll probably only have one config entry here - the last
time the news was marked as read by the player.</p>
<p>We&rsquo;ll store this as an <code>i64</code> and loading it is exactly the same as above.</p>
<p>The main difference with the <code>Game</code> config class is that on writing any value,
it will also save it to disk.</p>
```zig
pub fn markNewsAsRead(self: *Self) void {
    self.game.news_read = std.time.timestamp();
    saveConfig(self.allocator, self.game, self.game_path);
}

fn saveConfig(allocator: std.mem.Allocator, Config: anytype, full_path: []const u8) void {
    const path = std.fs.path.dirname(full_path) orelse ".";
    std.fs.cwd().makePath(path) catch |err| { // creates parent dirs if needed
        log.warn("unable to save: {any}", .{err});
        return;
    };

    var file = std.fs.cwd().createFile(full_path, .{
        .read = false,
        .truncate = true,
    }) catch |err| {
        log.warn("unable to save: {any}", .{err});
        return;
    };
    defer file.close();

    var writer = file.writer().any();
    toml.serialize(allocator, Config, &writer) catch |err| {
        log.warn("unable to write to config file: {any}", .{err});
    };
}
```
<p><del>I ran into a bug where
<a href="https://github.com/sam701/zig-toml/issues/32">the api for serialization was broken</a>.
There is (currently)
<a href="https://github.com/sam701/zig-toml/pull/33">a pending pr #33 to resolve it</a></del></p>
<p>One of the challenges of using emerging language and ecosystem is that you&rsquo;re
more likely to run into bugs. One of the great joys of working with such
ecosystem is the greater opportunity to contribute and get involved!</p>
<h1 id="links">Links</h1>
<ul>
<li><a href="https://youtu.be/OVswrFoFNjM">YouTube Devlog</a></li>
<li>Prev: <a href="https://icle.es/2025-05-20-crafting-machines.md">Crafting Machines</a></li>
</ul>
]]></content:encoded></item><item><title>Crafting, Mods, and Machines</title><link>https://icle.es/2025/05/20/crafting-machines/</link><pubDate>Tue, 20 May 2025 14:27:00 +0000</pubDate><guid>https://icle.es/2025/05/20/crafting-machines/</guid><description>&lt;p>In this post, I want to explore the crafting system in triangle, especially how
mods, tiers, and machine interactions could create depth without overwhelming
complexity.&lt;/p>
&lt;p>Crafting in triangle should be a mix of random number generation (rng) and
deterministic. Crafted items will have a random set of mods. However, like in
&lt;a href="https://lastepoch.com/">Last Epoch&lt;/a>, you can extract the mods of the items.
These can then be placed into another item.&lt;/p>
&lt;p>It feels like mod crafting would be an interesting way to approach crafting in
general. Unlike in the Last Epoch, the extracted mods aren&amp;rsquo;t just combined back
together.&lt;/p></description><content:encoded><![CDATA[<p>In this post, I want to explore the crafting system in triangle, especially how
mods, tiers, and machine interactions could create depth without overwhelming
complexity.</p>
<p>Crafting in triangle should be a mix of random number generation (rng) and
deterministic. Crafted items will have a random set of mods. However, like in
<a href="https://lastepoch.com/">Last Epoch</a>, you can extract the mods of the items.
These can then be placed into another item.</p>
<p>It feels like mod crafting would be an interesting way to approach crafting in
general. Unlike in the Last Epoch, the extracted mods aren&rsquo;t just combined back
together.</p>
<p>What if each mod retains its values? What if you could combine two mods together
to upgrade or reroll them?</p>
<p>When you insert a mod into an item, what if it destroyed the mod that you were
replacing?</p>
<h2 id="tiers">Tiers</h2>
<p>All materials, items, and mods have tiers. Factories will only process items of
their tier or lower. A Tier 1 smelter cannot process a Tier 2 material and a
Tier 1 constructor cannot craft a Tier 2 item.</p>
<p>I am currently mulling over the idea of having nine tiers.</p>
<h2 id="items-aka-buildings-machines">Items (a.k.a Buildings? Machines?)</h2>
<p>I&rsquo;m toying with the idea of the tier determining how many mod slots it could
have. A Tier 2 item would have two mod slots and a Tier 5 item would have 5
slots.</p>
<p>This provides a natural power curve, even if the mods themselves do not have
tiers.</p>
<p>All items will also have up to one implicit mod. This mod will be item type
specific. e.g., for smelters, it will be <code>smelting rate</code></p>
<p>From a concept perspective, there are probably buildings, or some kind of
structure that can be slotted on to the ship.</p>
<p>Perhaps buildings are a better name for them than items.</p>
<h3 id="slots--hardpoints">Slots / Hardpoints</h3>
<p>I see two kinds of slots on the ship</p>
<ul>
<li><em>Interior slot</em>: Machines like the smelters will go in here</li>
<li><em>Exterior Slot</em>: Weapons, Armour, Hull Extensions etc. could go on to these.</li>
</ul>
<h2 id="mods">Mods</h2>
<p>Currently mods also support tiers, but I am wondering whether the power creep
could become exponential, combined with the additional mod slots. There may be
good ways to mitigate it though, which will only become apparent once some of
these mechanics have been implemented.</p>
<p>Another reason to skip tiers on mods is if that means that the player ends up
with far too many mods in their inventory.</p>
<p>There are a couple of key areas in which I am thinking about deviating from
other games (like the Diablo series, PoE 1/2, Last Epoch etc.)</p>
<h3 id="local-only">Local only</h3>
<p>Each mod applies only to the item it is on. A firing rate increase on your
smelters won&rsquo;t do anything - since they don&rsquo;t fire. A smelting speed increase on
your weapon will likewise have no effect. It would make sense to extract or
replace the ineffective mods.</p>
<p>If you have two weapons attached, and one of them has a mod that increases
firing rate, that will impact only that weapon.</p>
<h3 id="no-prefix--suffix-mods">No Prefix / Suffix mods</h3>
<p>There is no classification of mods as prefix/suffix or limits of each type. You
can have full attack mods on if you like. Considering the previous restriction,
it makes more sense to stack attack on weapons and defense on armour.</p>
<h2 id="machines">Machines</h2>
<p>There are a few factory based machines I have in mind so far.</p>
<p>All these machines will have a rate at which they will complete their work. I
have been pondering whether higher tier machines will complete lower tier work
faster.</p>
<h2 id="smelters">Smelters</h2>
<p>The standard refinery. It will convert ore and scrap (obtained when enemy ships
are destroyed, and from scrapping items) into refined materials.</p>
<h2 id="constructor">Constructor</h2>
<p>Converts refined materials into items of Tier 1. For Tier 2 onwards, I am
considering taking items from the previous tier plus a refined material of the
new tier. E.g. to craft a Tier 2 factory, it could take 2 x Factory Mk. I + 30
Tier 2 materials (whatever that turns out to be)</p>
<p>This could keep the recipes easy to understand and reason about. It also
provides one use for poorly rolled items.</p>
<h2 id="disassemblers">Disassemblers</h2>
<p>These function similar to the Foundry in Last Epoch, at least with regards to
extracting all the mods from the item.</p>
<p>I haven&rsquo;t decided how deterministic it will be. Will it extract all items or
could some be damaged in the process and maybe turned to scrap?</p>
<h2 id="scrappers">Scrappers</h2>
<p>These are a bit like the trash bin. If crafting requires lower tier items, these
might not be necessary. TBD!</p>
<h2 id="foundry">Foundry</h2>
<p>This would be the machine that could do mod crafting. I am still a little foggy
about how or if this machine could work.</p>
<p>Should mod crafting involve risk? Like combining two mods to maybe create a
higher tier one - or failing and getting scrap?</p>
<p>What about rerolling a mod by sacrificing another mod?</p>
<p>All ideas accepted :)</p>
<h2 id="augmentor">Augmentor</h2>
<p>The Augmentor could be the final step in the pipeline - letting you insert or
fine-tune mods once you’ve refined or crafted them.</p>
<p>This machine will work instantly so that it doesn&rsquo;t tie up the items currently
in use. It would allow you to modify items up to its own tier.</p>
<h2 id="triangle">triangle</h2>
<p>With machines slotted directly onto the ship, triangle becomes a living,
drifting factory - salvaging, upgrading, refining, evolving, fighting, and
surviving!</p>
<h2 id="other-posts">Other posts</h2>
<ul>
<li><a href="https://youtu.be/livphL9lOxo">Companion vlog for this post</a></li>
<li><a href="https://icle.es/2025-05-13-materials.md">Prev: Materials &amp; Pickups </a></li>
<li>Next: Coming Soon</li>
</ul>
]]></content:encoded></item><item><title>Materials</title><link>https://icle.es/2025/05/13/materials/</link><pubDate>Tue, 13 May 2025 12:07:00 +0000</pubDate><guid>https://icle.es/2025/05/13/materials/</guid><description>&lt;p>In triangle, everything should feel earned - so all items are crafted, not
found. I am also a big fan of factory / automation games. How much of a factory
sim can we squeeze into an ARPG (or on to a triangle for that matter)? Let&amp;rsquo;s
find out!&lt;/p>
&lt;h2 id="drops">Drops&lt;/h2>
&lt;p>As a starting point, when asteroids are destroyed, they will drop materials.
I&amp;rsquo;ve also been pondering whether materials could drop when they split. It makes
logical sense, but I want to get a better sense of pacing before diving into
that question.&lt;/p></description><content:encoded><![CDATA[<p>In triangle, everything should feel earned - so all items are crafted, not
found. I am also a big fan of factory / automation games. How much of a factory
sim can we squeeze into an ARPG (or on to a triangle for that matter)? Let&rsquo;s
find out!</p>
<h2 id="drops">Drops</h2>
<p>As a starting point, when asteroids are destroyed, they will drop materials.
I&rsquo;ve also been pondering whether materials could drop when they split. It makes
logical sense, but I want to get a better sense of pacing before diving into
that question.</p>
<p>Currently, only iron drops. I started off by finding a sprite sheet with
reasonable looking sprites and then started integrating them in before I decided
to just stick with shapes — so iron drops are little blue circles.</p>
```zig
var shape = Shape.initCircle(7);
shape.move(pos);
shape.colour = .blue;
```
<p>PS: you haven&rsquo;t seen the <code>Shape</code> code, but you get the gist.</p>
<p>These drops are managed by a <code>MaterialField</code> that keeps track of all the dropped
materials. They are given a low linear velocity, and no rotational velocity
(it&rsquo;s a circle, so you wouldn&rsquo;t see it anyway).</p>
<p>They do not interact with collisions to avoid them being bumped off the screen
or you having to chase them down.</p>
<p>At some point, they could also be optimised to only update if they are on
screen. Assuming the player will pick up most of them, this may not be
necessary.</p>
<h2 id="pickup">Pickup</h2>
<p>I also &ldquo;installed an attractor&rdquo; on the ship such that if it gets close enough to
a material, it&rsquo;ll pull it in. When it&rsquo;s close enough, it&rsquo;ll be &ldquo;picked up.&rdquo;</p>
```zig
pub fn pickup(self: *Ship, material: *Material.Drop) bool {
    const magnet_sq = self.magnet_range * self.magnet_range;
    const pickup_sq = self.pickup_range * self.pickup_range;

    const dist = self.pos().sub(material.body.pos());
    const dist_sq = dist.magSquared();

    if (dist_sq <= pickup_sq) {
        self.save.inventory.addMatDrop(material);
        return true;
    }

    if (dist_sq <= magnet_sq) {
        // pull it towards us
        material.body.applyForce(dist.scale(self.magnet_force).sub(material.body.lvel));
    }

    return false;
}
```
<p>It is a physics interaction, so it is possible to slingshot the material and
have it fly off the screen (it&rsquo;s happened to me). It feels like a sun
interaction and you are in control of it, so I am inclined to leave that in.</p>
<p>Once it&rsquo;s close enough, it&rsquo;s picked up, and added to an inventory.</p>
<p>The attractor also feels like an upgrade that can be crafted later.</p>
<h2 id="smelting">Smelting</h2>
<h3 id="crafting-panel">Crafting Panel</h3>
<p>For smelting, I started by sketching out a straightforward UI. I&rsquo;ll admit up
front that I don&rsquo;t enjoy UI work, but in earnest I started on it, and I got as
far as displaying the panel itself and I got tremendously bored.</p>
<p>
  <figure>
    <img src="/assets/2025/05/craft-ui-sketch.png" alt="Crafting Panel Sketch" class="figcaption-img">
    <figcaption>Crafting Panel Sketch</figcaption>
  </figure>

</p>
<p>I decided to make material refinement prioritisation automatic, at least for the
time being. It would always prioritise the most valuable material that is in the
inventory.</p>
<p>Actually, that&rsquo;ll remove some factory micromanagement without removing the
feeling of agency from the player.</p>
<p>It&rsquo;ll mainly be a problem if the player has run out of lower tier materials and
they have a large amount of higher tier items in the inventory. I figure let&rsquo;s
wait until we have some playtesting before we worry about it. The system
currently only drops one material and it can be refined automatically.</p>
<h3 id="refining">Refining</h3>
<p>All materials and items will have a tier, starting with iron at 1. To be able to
smelt a material, you&rsquo;ll need a smelter at a minimum of that tier. For example
to craft iron, you need a minimum tier 1 smelter (which is fine since 1 is the
minimum tier anyway). If you wanted to smelt a tier 3 material, you&rsquo;ll need a
minimum of tier 3 smelter.</p>
<p>The ship starts with a Tier 1 smelter and will always have a tier 1 smelter.
This smelter cannot be modified, removed, or destroyed. The same is true of some
other factories, which will be covered in the next post.</p>
<p>The first iteration of refining took the total capacity of all smelters at each
tier, and would convert a portion of the materials from <code>ore</code> to <code>ingot</code>. It
used floating points to track progress. If it would take four seconds to convert
one ore to ingot, over one second, it&rsquo;d transfer 0.25 from ore to ingot.</p>
<p>The display only showed integers, so there was a bit of <code>floor</code> and <code>ceil</code> to be
able to show consistent numbers.</p>
<p>I didn&rsquo;t like this way of doing things. It felt icky — hacky — but it was simple
enough and it works, for now.</p>
<h2 id="other-posts">Other posts</h2>
<ul>
<li><a href="https://youtu.be/8ct9aWNj3Zk">Companion vlog for this post</a></li>
<li><a href="https://icle.es/2025-05-10-asteroid-field.md">Prev: Procedural Asteroid Fields</a></li>
<li>Next:
<a href="https://icle.es/2025-05-20-crafting-machines.md">Refineries, Constructors and other other factory types in a world without conveyor belts.</a></li>
</ul>
]]></content:encoded></item></channel></rss>