Devlog 5 - Custom tools are helpful


Starseed Harmonies might be the first game for which I developed tools for myself. Although I made some utility components for Sound Horizons, those were more like abstractions. I already had Unity and Wwise GUI to help me design the game. But Starseed Harmonies is a code-only project. The level-design can only be edited in JSON files, which is not ideal at all to visualize and iterate! Especially given the large scope of the game.

So I needed a home-made tool to easily edit the game's levels, and convert them into JSON data. At first, I thought of building a complete editor, as some kind of administration page. After all, the game already has a grid, surely I could re-use it in a special edition mode to position tiles, set their prices, durations, and other properties. But the more I thought about it, the more it seemed that it would be a significant cost.

So instead I tried to find ways to make the cheapest automatisation possible. If I was working with a team of level-designers, providing them efficient tools would be necessary. But I'm working alone, so I can make some compromises! As long as I find a workflow I'm comfortable with, having a weird (and undocumented) UX is okay.

The tools I made for Starseed Harmonies have this in common:

  • They were very fast to code. Probably less than an hour each. They are here to automatize repetitive or dull tasks.
  • Their code is dirty. While for Starseed Harmonies' code I take care of typing everything and making components and functions elegant and readable, it wasn't necessary to be as strict for the tools. They are for me only and have only one purpose. So they are only partially typed, they are not optimized, and certainly not modular. Who cares if the code sucks? They are disposable. As long as they do what I need them to do, it's fine.
  • They are commands run in terminal. Just a script in package.json. Some could have been a web page, but I know myself: I'd lose time styling it just for the sake of it! Calling a function in a terminal is simpler to implement.
  • Their output must be copy-pasted. Told you it was a weird UX. It's dirty, but efficient! I know where the result should go, and I'm comfortable with multi-cursor editing to quickly insert it in the code.
  • They can be used multiple time. These tools serve for level-design, generating tiles, colors… I need them each time I add new instruments or fields. They save me a tremendous time, and were really worth the (small) investment!

This is very abstract for now. Let me present the tools I created to give you a better idea.

Field layout generation

When I started the development of Starseed Harmonies, I defined a model for the Tile and Field configurations. This is how tiles are defined:

{
  name: "beige-bass",
  position: { x: 3, y: 2 },
  plantation: {
    cost: {
      type: "circle",
      value: 1
    },
    gain: {
      type: "circle",
      value: 1
    },
    duration: 8
  }
}

Every tile needs a configuration object like this, with notably a unique name and position. Field also have their own configurations, with a grid property that lists all the tiles by names.

At some point, I had composed a bunch of music, and was ready to test the first prototype. However, for everything to work, I had to configure every tiles of every fields. Doing this by hand seemed way too laborious, especially if it was just to test if the engine was working.

So my first solution was to generate tiles. I wrote a function that only takes the name and type of the tile, and generates a default configuration. Same position and same values for all of them. I made a script that run this function with a list of names, put the result in an array, and print it in console. I was then able to copy-paste it in the appropriate source file. Sure, not ideal, but just what I needed for prototyping.

With one exception though: the positions weren't set. They were all 0/0. To try the prototype further, I needed to randomize them. There's a constraint though: each randomized position had to be unique in the grid. I first wonder if a LLM could do such a task. I quickly understood that no, they can't (at least with Copilot). Not only does it sometime repeat the same position, it is also not creative at all! Despite insisting on creating random positions, it would always align them starting from top left… Writing an algorithm for this was much more efficient, and very simple.

At this point, the tiles configuration were still written in a TS source file. It would have been difficult to edit it through a script. So once again, I printed the results in console. I just edited the script manually to specify how many positions I wanted, and it would give me an array. With multi-cursor, it was then doable to copy-paste them in their own respective tiles. I kept this workflow for a good part of the alpha. I only needed random positions for the tiles, and generating them this way was fast enough.

But at some point, of course, I needed more control.

Field editing

As much as I enjoy serendipity, I couldn't let the tiles positions be random forever. Not only are there some constraint in the game-design that requires the tiles to have cohesive positions, but it's also visually more pleasant if tiles a grouped in clusters. So I needed a way to edit tiles positions. Sure, I could edit it in code directly. But can you imagine a file full of configurations just like the one above? There are more than twenty tiles in a single field. Trying to visualize how they are placed relatively to one another only by reading coordinates is just impossible. I need a GUI with a grid that I can edit directly to move tiles around!

I was once tempted to develop a level editor. It was taking a huge place in the planning, but seemed necessary. But then I had the idea: what if I used an existing spreadsheet software instead? Plenty of games use text files to define their level-design. Could I do the same with CSV?

I made two scripts: one that reads the tile's configurations and write positions in a CSV, and another that reads the CSV and updates the positions in the configurations. To do this, I had to move the configurations into JSON files. I kinda lose the typing this way, but the resources are more organized. And more importantly, I can now manipulate and update these JSON files with a script. These files contain all the properties as shown above, while the CSV only indicates the position of each tile. It thus allows me to edit each field directly in LibreOffice Calc!


CSV is pretty limited though. So to better organize myself, I now use a separate spreadsheet where I use colors to mark important informations and better visualize how a level is spread out. Then I just have to copy it into the CSV.


The 2 scripts take a single parameter: the name of the field. It allows them to know which files to read and write. Plus, they are typed, so if I mess up the name I just get an error.

// Write CSV for blue field
npm run gen-level-csv blue
// Update positions in blue.json
npm run set-level-positions blue

This is how I create a new field now: I list the tiles names, generate the default tiles with random positions, write it in a JSON file, then convert that into a CSV, edit it, and finally convert the CSV back into the positions in the JSON. After that I just have to do manual adjustments on each tile for their special properties (like how much they cost, how long they grow, etc). And the field is ready to play with. An efficient workflow without the need of a special level editor!

Color conversion

This is something I've already talked about in a previous devlog. The sky background uses a shader to which I have to pass colors in code. The colors use a RGBA format with an array of 4 numbers:

type ColorVector = [number, number, number, number];
const BEIGE_1: ColorVector = [0.17647058823529413, 0.1411764705882353, 0.09019607843137255, 1];
const BEIGE_2: ColorVector = [0.5254901960784314, 0.44313725490196076, 0.3176470588235294, 1];
const BLUE_1: ColorVector = [0.07058823529411765, 0.13333333333333333, 0.19215686274509805, 1];
const BLUE_2: ColorVector = [0.17254901960784313, 0.30980392156862746, 0.47843137254901963, 1];

Naturally, this isn't great to edit. In the CSS theme, the colors are in hexadecimal ('#2d2417', '#867151', …), a clearer format both for the IDE and a human reader. So I wrote a script to convert them. In the script I just write an object of hexadecimals colors, then it converts them to arrays and print them all so that I can copy them in the code.

However, this time it wasn't really practical. The sky's color arrays were not only completely separated from the theme, they were also unreadable! This made them pretty difficult to maintain. Every time I needed to change them in the CSS, I had to also edit the script, execute it, copy its result… And unless I run the project and see it for myself, I couldn't tell if the values were correct. It made editing colors or creating new ones too laborious.

So I changed plans and instead exported the script as a utility function that is executed during runtime. The colors are written in hexadecimal, easy to edit and read. Every time the game starts, it generates the arrays for the shader. So technically, it makes the code less optimal. But it's not like this is an expensive operation!

This might be an exemple of a tool that wasn't really needed, because it required too much manual intervention. I guess that if something can be run directly by the game, it's good to consider that option. I could also reflect on the input and output of this script: its goal was to take a readable input (hexa color) and transform it into an unreadable output (array of float). I think this was a mistake: most programs purpose is to take input written by human to eventually output a result for human as well. This is what my other tools do: even though JSON is impractical for editing positions, it's ideal for editing other properties! Writing an undecipherable value into my code was a bad choice. The "raw" parts should remain hidden so that I can easily edit data I can understand.

What about AI coding agents?

If you're a web developer in 2025, it's difficult to avoid coding agents like Claude, Cursor or Copilot. There's a lot to talk about the capabilities and ethics of AI, but I can recognize that when used correctly they can greatly improve productivity (to some degrees). Now, Starseed Harmonies is thankfully not vibe-coded. But how much did I use AI tools for it, and did they help?

The answer is: not much. I'm using VS Code with a free Copilot, and as of today, Copilot isn't very good with Svelte. Not only does it have less exemples to take inspiration from, but on top of that it keeps mixing syntaxes of different versions! I disabled the inline suggestion, as I usually consider it to be too distracting and a waste of time to fix. But I didn't use the agent either, since its code were most of the time incorrect, and never optimized. Plus, Starseed Harmonies is a game project, not a web app. Meaning that it does weird things that you don't typically see in web projects. Thus Copilot had a hard time to predict were I wanted to go, and how I needed the code to be.

There were times where it was useful though. For example, for coding some of the aforementioned tool. I told you I didn't need their code to be perfect. And their task are simple, within a precise scope. It was thus efficient to write down them spec, and let Copilot make a first draft. For some of them, I had to make adjustments and fixes. But these tasks were simple enough to make the use of the agent efficient, and quickly obtain a script that I could try.

There was however a recent use case for Copilot that has proven successful: automating the initialization of a new field. With the tools in place, this part is not really creative. Once I have all the music files ready, I have to write the list of their names in a file, update type somewhere else, declare  gradient of colors, write configurations, link to file paths, etc. There are several files that just need to be updated, sometimes in just one line, to set everything in place. This includes the use of the scripts I mentioned above: generating positions, writing  CSVs, updating JSONs… It's a series of very small task, not particularly tedious, but certainly time consuming. It was also easy to forget one step in the process!

So some weeks ago I decided to write a custom command for copilot describing all the steps needed to integrate new tiles in new fields. It would at least had the benefit of having a documentation for myself. But it turned out to work great with Copilot! It's not perfect, sometime Copilot would forget to execute a step, or misunderstand one of the input. But when done step by step, it's easy to review and fix. I also improved my prompt when it seemed to be too ambiguous. And with that, I have a pretty neat workflow to create new fields! Just a command to run and some arguments to provide that executes a todo list. Manual intervention is still required (for level-design, play-testing, adjusting colors…). For now, I can't say that it was a significant gain of time. But now that it is ready to be used again, it might be in the future! And at least, it greatly reduces the cognitive charge I had for adding new tiles.


For what was supposed to be a technical-demo, I ended up building a nice dev environment for this game! I guess my pet peeve is to always try to write abstractions and optimize code. Hey, when I don't have any dead-line, it's a fun exercise! But here for once it's helping me getting faster to the end. With the right amount of compromises, these tools help to implement content faster while keeping the code base clean.

In fact, I'm pretty sure the next update will come very soon. I almost don't have time to write these devlog between them! It's nice when development goes this smoothly.

Leave a comment

Log in with itch.io to leave a comment.