Skip to main content

Home

post

Sources and Sinks: Premium Currency on Cobblemon Servers

Back while I was working on Roanoke Diamond, I wrote up an in-depth analysis of their economy and suggested improvements. I focused primarily on their premium currency as method to reward players for their dedication and engagement. These are the lessons I learned:

A single source produces inequality

If a server only has one in-game source of premium currency, whichever players engage with that source will hoard everything. An example is weekly tournaments. They're fun, they're high-effort, and rewarding them with currency makes sense. However, if tournaments are the only repeatable source, the same few competitive players will accumulate large sums while everyone else has zero.

This has cascading effects. New players who want to compete look at the gap and feel locked out before they start. Players uninterested in PvP have no way to participate in the economy at all. Currency loses its meaning as a marker of dedication when only one type of dedication counts.

The lesson is to think about which player archetypes you actually want to reward, and make sure each one has a route in. A reasonable starting set:

  • Competitive players: tournament placement and participation.
  • Quest runners: weekly tasks, story milestones, dex completion.
  • Achievement hunters: vanilla Minecraft advancement triggers, breeding goals.
  • Event participants: seasonal events with currency spread across event tasks.
  • New players: a tutorial questline that introduces both the server and the currency itself.

A brand-new player should be able to earn enough in their first session to buy something meaningful. That single act of spending teaches the entire economic loop in a way no chat message can.

Pick a weekly target and balance around it

Once multiple sources exist, you need an estimate of how much currency an average engaged player earns per week. This number is the anchor for everything downstream. Without it, sink pricing is guesswork and the economy drifts.

My suggested approach: pick one source (often repeatable weekly quests) and treat it as the baseline. Decide how much that source pays out per week, then size other sources relative to it. Tournament first-place might be worth one week of quests. A seasonal event might be worth two weeks. A dex rank-up might be worth a third.

Sink prices come from the same anchor. A premium cosmetic should feel like a real commitment, maybe two weeks of dedicated play. A high-end decoration block might be three or four. Cheap consumables like ball swappers might be a tenth of a week. If the anchor changes later, everything else has to be re-tuned in proportion.

It should be noted that this calculation only works if you actually log earn and spend data per player. Without that, you're guessing at how the economy feels.

Sinks decide what the currency means

Sources determine who earns the currency. Sinks determine what it stands for.

A cosmetic-only sink (skins, hats, decoration blocks) sends the message that currency is about self-expression and showing off dedication. A gameplay-power sink (rare Pokémon, stat items, shiny boosts) sends the message that currency is about competitive advantage. Most servers want both, and the mix should be deliberate.

Here's what I consider to be a good balance:

  • A large and diverse amount of Cosmetic Sinks. Custom skins, hats, decoration blocks. These should make up the bulk of what currency can buy, with prices high enough that completing a set is a long-term goal.
  • Convenience sinks are the cheapest tier. Ball swappers, mints, small boosts. These exist so players have something to spend on early and feel a regular sense of "I earned enough to get the thing.". Keeping them cheap helps new players get a basic team built quickly.
  • High-power sinks stay rare and expensive. Ability Patches, stat-boosting items, signature Pokémon. Available, but priced so that earning them through gameplay is a real accomplishment.
  • Decoration blocks as the prestige tier. If your server supports Polymer or similar custom-block tech, decoration blocks are the perfect endgame sink. They're visible (other players can see them in claims), they're collectible, and they have no upper bound on quantity.

I think there's also a case for keeping at least one category of decoration sold for in-game currency only, with no real-money path. It gives dedicated players something concrete to show for their time that money can't buy.

Utilize your backlog

Servers lucky enough to have many artists making custom textures can often accumulate a backlog of skins that are hard to collect. They come from texture jams, seasonal events, one-off commissions. After the event passes, those skins effectively vanish.

A rotating skin shop is a great way to bring them back. Pick a quarterly rotation, show off the catalog on your website with artist credits, and let players buy what's currently in the rotation. Skins that age out of rotation can move to a permanent shop at a lower price.

This serves several goals at once. Players get a steady stream of new-to-them cosmetics. Artists get visible credit for work that would otherwise sit in a folder. The server gets a recurring reason for players to log in and check what's available. And the existence of the skin shop contextualizes any real-money skin sales (the in-game price acts as an anchor, so a real-money offer reads as a time-saver).

Staying F2P-friendly

The strongest reason to build a real in-game currency economy is that it lets you honestly claim the server is F2P-friendly. Every cosmetic, every boost, every Pokémon. All of it should be reachable through play alone.

This works because it changes the meaning of real-money purchases. Players spending money are buying time and convenience, with full knowledge that the F2P route exists. That is a more comfortable transaction for everyone involved than the version where certain items are paywalled forever.

For these reasons, I'd push back hard against any item being real-money-exclusive. If it can be sold for money, it can be priced in currency too. The cosmetic gacha pack that drops next month should also be available, individually, in the shop later. Holdouts here are where F2P-friendliness quietly erodes.

What I'd build first

If I were standing up this kind of economy on a new server, I'd tackle it in this order:

  1. Pick the weekly currency target and the anchor source.
  2. Wire up two or three additional sources at different engagement levels.
  3. Put one cheap consumable in the shop so new players have an immediate goal.
  4. Ship the cosmetic shop, even with a tiny inventory.
  5. Add the website catalog so the shop is browsable from outside the game.
  6. Iterate on rotation and pricing based on actual earn and spend data.

Everything else (decoration tiers, retroactive milestone rewards, prestige cosmetics) is downstream of having the basic loop running.


The core insight, if there is one: a premium currency is primarily a design tool. The monetization follows from doing the design well.

skin

Luminous & Exalted Qwilfish Line

Luminous variants of Qwilfish and Overqwil with deep navy and bright cyan scales.

Exalted variants of Qwilfish and Overqwil with crimson and orange flame-like spines.

Originally made for Callisto shiny variants.

project

Cobblemon Karp of the Week NPC

A weekly Cobblemon quest built around the 31 Magikarp Jump forms. The Karp Collector NPC picks a form for the player each week, gives them a biome hint and a Journey task, and trades them 'Karp Coins' (KC) and shop unlocks when they bring the right karp back. Lifetime progress is tracked through milestone titles and cosmetic/tool unlocks. I made it to give players more of a reason to collect all 31 forms, and to add content specifically tied to fishing, which is usually my favorite activity in any game that adds it!

Karp Collector

First visit

The Karp Collector NPC offers a starter quest the first time the player interacts with it: bring me one normal Magikarp. On submission, the player receives 10KC, access to the shop, a warp unlock to the Collector's location, and entry to the weekly loop.

Weekly loop

Each week, the Collector picks a Jump form for the player. Selection prioritizes forms the player has not yet submitted. Once all 31 normal forms are in, selection switches to forms the player has not yet submitted a shiny version of. The player is given:

  • A description of the karp's appearance.
  • A biome hint pointing them toward where to find it.
  • A Journey task auto-assigned for tracking in /journal.

On submission, the Collector trades the player 5KC for a normal Magikarp, or 25KC for a shiny one. The form's trophy is also unlocked in the shop. Shiny submissions unlock both the normal and shiny trophies for that form.

The week resets at Sunday 00:00 server time for all players simultaneously. The default is UTC, but the timezone can be adjusted with an offset.

Rewards

The Karp Collector's shop is buy-only and gated by permissions granted as the player progresses. It is implemented in SkiesGUIs. The cosmetics and tools listed below are placeholders rather than functional items. (at time of writing)

  • Trophies (62 total, one per form, normal and shiny):
    • Normal: 4KC
    • Shiny: 20KC
  • Resources (ungated):
    • $1k: 1KC
    • IV Candy: 1KC
  • Cosmetics (unlocked at 5/10/15/20 unique forms submitted):
    • Fishing Boots: 4KC
    • Fishing Waders: 8KC
    • Fishing Vest: 12KC
    • Fishing Hat: 16KC
  • Tools:
    • Karp Rod: 20KC (unlocked at all 31 normal forms submitted)
    • Prismatic Rod: 40KC (unlocked at all 31 shiny forms submitted)

Titles

Lifetime weekly completions award milestone titles:

  • title.fisher on the first normal submission.
  • title.angler on the first shiny submission.
  • title.karp_collector for all 31 normal forms.
  • title.karp_connoisseur for all 31 shiny forms.
  • title.karp_cadet / crewman / commander / captain / colonel / king at 5, 10, 20, 30, 45, and 60 weekly completions.

Fish tank

A fish tank is built behind the Collector. As the player submits each form, that exact karp variant appears swimming in the tank, visible only to them via per-player NPC visibility. A completionist's tank shows all 31 forms swimming in their own personalized aquarium.

It should be noted that the fish tank can ship after the core quest, since the gating permission (npc.kotw.submitted.<aspect>) is set on submission whether or not any tank NPCs exist yet.

Special tools

The Karp Rod and Prismatic Rod are aspirational long-term goals, not yet implemented:

  • Karp Rod: Boosts the chance of reeling in a Jump form. Doubles the shiny chance on reeled-in Magikarps.
  • Prismatic Rod: Never reels in Magikarps. Doubles the shiny chance on every other reeled-in species.

These are intended to be rare endgame items. The Prismatic Rod especially requires every shiny form to unlock, which will take a dedicated player a year or more.

Techy Stuff

  • Built on Cobblemon's NPC, dialogue, and behaviour systems, configured in JSON.
  • Quest logic written in Molang. The weekly reset is gated by a next_reset_date string in q.player.data compared against q.date_is_after, rather than a server-wide cron job.
  • Form selection draws from the player's un-unlocked pool first, falling back to shinies-only once all normals are in, and then to anything once both sets are complete.
  • Server-side dependencies: Journey (task tracking and per-player visibility), LuckPerms (progression gates), Impactor (karp_coins currency), and SkiesGUIs (shop interface).
  • Trophies, the Karp Coin item, and the aquarium NPCs are modeled and textured in Blockbench as filament assets, served through Polymer so vanilla clients see them without installing a resourcepack.

It should be noted that the Fishing Cosmetics and Special Rods cannot be implemented purely via datapack and require a dedicated companion mod. Servers that do not want to install a sidemod can still ship the entire core quest, the shop, all 62 trophies, the titles, and the milestone system.

Built for the Callisto and Cobblewilds Cobblemon servers, but available for commission.

skin

Luminous & Exalted Pineco Line

Luminous variants of Pineco and Forretress with brown bark.

Exalted variants of Pineco and Forretress with bright purple bark and glowing core.

Originally made for Callisto shiny variants.

skin

Luminous & Exalted Murkrow Line

Luminous variants of Murkrow and Honchkrow with bright cyan plumage.

Exalted variants of Murkrow and Honchkrow with grayscale "noir" styling and bright red accents.

Originally made for Callisto shiny variants.

project4

Cobblemon Model Viewer

Interactive 3D viewport that renders Cobblemon Bedrock models (.geo.json) with their textures and overlay layers. Pass a species and aspects — the resolver automatically selects the right model, base texture, and any emissive or translucent overlays. Drag to rotate · scroll to zoom · auto-rotates.

Resolver system

Resolver JSON files in public/cobblemon/bedrock/pokemon/resolvers/ map aspect combinations to models and textures. Each resolver has an order field; multiple resolvers per species are loaded and sorted by it (lowest = highest priority). Variation matching requires all listed aspects to be present in the active set, preferring more specific matches (more aspects = higher score), with resolver order as a tiebreaker. Layers are typed as emissive, translucent, or emissive + additive (a 0–1 strength number), each resolved to a public URL via cobblemon: namespace stripping.

Bedrock geometry loader

A custom loader parses .geo.json without any third-party Bedrock library:

  • Coordinate mapping — Minecraft +Z south maps to Three.js −Z, so all Z positions are negated. Bone rotations use (+rx, −ry, −rz) with ZYX Euler order to match Blockbench's internal Z-negated frame (conjugated by an implicit Y180 flip relative to Blockbench's own loader).
  • Bone hierarchy — each bone becomes a THREE.Group positioned at its pivot. Cube meshes are offset by −pivot so their vertices land at model-space coordinates regardless of nesting depth. Cube-level rotations get their own wrapper group positioned at the cube pivot.
  • Box-UV unfolding — the side strip follows [west][front][east][back] order (not the Java skin convention). Bottom-face UV winding is corrected by forcing BL = (x1, z1) and inverting the flipU flag relative to the other faces.
  • mirror_uv — mirrors east/west rects AND flips the U axis on all six faces, matching Blockbench's source behavior. Used on mirrored cubes like Pidgeot's wings and eyes.
  • Per-face UV — per-face uv / uv_size specs are supported alongside box-UV; inflate pads all six faces outward.

Material and layer blending

Geometry is built once and shared across the base and all overlay layers — only materials differ. Layer blending:

  • BasealphaTest cutout, depth write enabled.
  • Emissive — full-bright (not additive); NormalBlending with cutout alpha and no depth write. MeshBasicMaterial is already unlit, so AdditiveBlending would only saturate toward white.
  • Emissive + additive: <number>AdditiveBlending with opacity set to the 0–1 strength value, scaling how much the layer adds to the scene. opacity multiplies the source alpha before the GPU blend equation, so it's a direct brightness control with no shader changes needed. Used for Lapras's crystal tips (0.6).
  • TranslucentNormalBlending with full alpha channel, no depth write.
post

Hello, World (again)

Welcome to the fourth major iteration of my website! Let's take a quick trip down memory lane:

Version 1: Handwritten HTML, CSS, and JavaScript

first website gif

The first version was entirely hand-crafted HTML and CSS, both of which I was pretty new to. I was taking a couple classes on the subject and used this site mainly to test what I was learning. Features included an overcomplicated php blog, a very simple html5 canvas game, and dancing ascii kirby!

Probably the coolest part of this site was that it had a rotating background image. I wrote a python script to change the image at midnight, and then called that python script as a server job. There were even special backgrounds for holidays! I didn't own these images of course, and maintaining my own html/css for every page quickly became too much work for me to update it frequently. After a few years I refreshed it:

Version 2: Static Site via Jekyll

second website screenshot

This version was built with jekyll and hosted on github pages. Discovering the world of static website building was amazing, and I didn't even have to pay for hosting now. I grabbed a pretty theme, modified it just enough, and posted my new homepage. This time around I focused on linking music, videos, and websites I enjoyed. The only updating I did was changing the video occasionally.

Version 3: Static Site via Hugo

third website screenshot

My motivation this time was to learn hugo and construct a portfolio. I was trying to find my unique style, and you may notice that some of the key design decisions of this current site had their start here!

Version 4: Next.js (current)

We've finally arrived at the site you are currently viewing. I had three motivations for this refresh:

  1. Use Next.js and Vercel for a more fully-featured React-based stack.
  2. Showcase all the little things I've worked on that didn't necessarily fit the previous site's big-project focus.
  3. Surface Cobblemon-specific work that I do for commission.

What's Next?

Expect more little updates as I continue to work on this site and my Cobblemon server projects. Blogs will be few and far between, but I'll try to write them for larger projects that surfaced deeper lessons.

Thanks for visiting, I hope you find something here to love!

project

Cobblemon Cannon NPCs

A custom Cobblemon "Cannon" NPC that players can push, aim, and launch themselves out of. Server owners customize each cannon's appearance and behaviour, so the same NPC fits a Pokémon gym puzzle, a hub-transport launcher, or just a piece of arena dressing.

What it does

Right-clicking a cannon pushes it one tile in the direction the player is facing on the world grid, and can be configured to move multiple blocks at a time. Shift+right-click opens a menu instead:

  • Rotate — turn the barrel to the next snapped angle. The rotation increment is configurable per cannon, so a server owner can pick whatever step fits the puzzle.
  • Launch — fire the player out along the barrel's current aim.
  • Reset — return the cannon to its home position and teleport the player back to a saved spot. Both positions are set by the server owner.

Push, rotate, and launch can each be disabled or gated behind a permission per cannon so the same NPC can act as a decorative prop in one place and a permission-gated fast-travel launcher in another.

What you can build with it

Because each instance is highly customizable, the cannon NPC can take on a lot of different roles:

  • Gym puzzles — the type-themed variants exist mostly so a cannon can drop straight into a Pokémon gym whose puzzle is built around aiming and launching the player.
  • Movable puzzles — push and rotate together let players solve room-scale puzzles by lining the cannon up and firing themselves through gaps or onto otherwise-unreachable platforms.
  • Hub transport — a cannon at spawn that launches players toward a distant area replaces a long walk with a satisfying arc.
  • Decoration — the type-themed reskins make it easy to match a cannon to a gym, biome, or themed build without commissioning new art.

Type variants by Croix

The cannon ships with one base "circus" form and 18 beautiful Pokémon-type repaints, all made by Croix. The variants exist primarily so the cannon can be dropped into a Pokémon gym puzzle and immediately read as part of that gym's type. The base model only had to be unwrapped once; every type variant slots into the same atlas.

The trajectory math

The trickiest part of the project turned out to be the physics. Computing the right launch velocity for a given barrel angle is ordinary 2D projectile motion, until you remember that Minecraft's coordinate system is right-handed, meaning Z increases as you go south rather than north. I had to apply the cannon's current rotation to the player, and then derive the correct forward vector inside that convention rather than the one a high-school trig class assumes. It was a nice excuse to actually reach for a bit of geometry and linear algebra in a creative project.

Tech

  • Built on top of Cobblemon, the Pokémon mod for Minecraft, using its built-in NPC, behaviour, and dialogue systems (all configured in JSON).
  • Core push / rotate / launch logic written in Molang, Bedrock's lightweight scripting language.
  • Model and animations built in Blockbench as a Bedrock geometry, with idle / fuse / shoot animations driven by Cobblemon's poser system.
  • Requires the Journey mod, which supplies the functions to launch the player, as well as apply the fall-damage-immunity buff so the player survives the landing.

Built for the Callisto and Cobblewilds Cobblemon servers, but available for commission.

project

Cobblemon NPC Dialogue System

A composable dialogue system for Cobblemon NPCs. Two base behaviours handle the conversation itself: Simple Dialogue for one of two lines, and Long Dialogue for one of two 1–10 line sequences. A suite of optional "quest addons" (battles, item trades, currency trades, and Pokémon trades) attach to the end of any compatible dialogue. A server owner can quickly build a talking npc, quest giver, or battler entirely from in-game editor variables, without writing any scripts.

The system is permission-driven, with every branch and every addon reading from and writing to LuckPerms. As such, dialogue state is visible to admins, scriptable from outside the NPC, and scoped per-player with no extra bookkeeping.

The two base dialogues

Simple Dialogue displays one of two configured lines. The active line is gated by a line_switch_perm. If the player has the perm, they see the alt line. Otherwise they see the default. After the line is shown, a seen_perm is granted. By default, this is the same perm that flips the switch, so the second visit reads as a follow-up:

First visit: "Hello, I'm Mary. Nice to meet you!" (perm granted on close)

Second visit: "Hey . How's it going?"

Long Dialogue does the same thing with 1–10 lines per branch. The lines are clicked through one at a time, default and alt branches are gated the same way, and a separate seen_perm per branch tracks completion. Empty line slots mark the end of the conversation, so if line 4 is empty, the conversation ends after 3 lines.

Both support {{npc}} and {{player}} placeholders everywhere, and both write through the same set_dialogue_interaction hook. As such, the optional quest addons treat them interchangeably.

The quest addons

Each addon is a separate behaviour added to an NPC that already has a dialogue behaviour. When the dialogue runs, the addon's button can appear at the end of it. The four addons available:

  • Optional Battle: adds a "Battle" button. Has an optional required_perm gate, a configurable confirm dialogue, and hooks into the standard battle resolution callbacks.
  • Optional Item Trade: adds a "Submit Items" button. Configurable required item (or defined item), amount, completion command, completion script, completion perm, and a post-trade dialogue line.
  • Optional Currency Trade: same shape, but takes Impactor currency instead of items. Defaults to impactor:dollars.
  • Optional Poke Trade: same shape, but takes a Pokémon matching a configured species/aspect string.

Every addon shares the same three completion outputs: a perm is granted, an optional command runs, and an optional Molang script runs. As such, quest chains are straightforward to build. Completing a poke trade can grant a perm that unlocks the alt dialogue branch on a different NPC, which offers an item trade, which grants another perm, and so on.

After-dialogue extensions

Three additional behaviours fire after a long dialogue concludes, regardless of whether any quest addons were triggered:

  • After Dialogue Command: runs a vanilla /-command.
  • After Dialogue Script: runs a Molang script.
  • After Dialogue Forced Battle: starts the standard force-battle flow as soon as the dialogue closes.

These are independent of the optional trades. A single NPC can offer a Pokémon trade and always run a command after the conversation, in either order.

Use cases

  • Shopkeepers: long dialogue plus optional currency trade with a give command on completion.
  • Quest givers: long dialogue with a perm-gated alt branch, plus an optional item trade that grants the next quest's perm.
  • Battlers: simple dialogue plus an after-dialogue forced battle, plus a battle_won_perm that switches the alt branch for next time.
  • Branching state across NPCs: chain seen-perms so a conversation on the other side of the map opens up new dialogue at home.

Why permissions

The dialogue system is intentionally permission-driven. Every branch and every quest addon reads from and writes to LuckPerms. This has three big advantages:

  1. State is visible to admins via /lp user X permission info.
  2. State is scriptable from outside the NPC. A Journey task completion can grant the perm that flips a dialogue branch, and vice versa.
  3. State is scoped per-player by default, and located in LuckPerms where it can be easily fixed if anything breaks.

It should be noted that the addons are also designed to be additive. An NPC can offer a battle and a trade and run a command afterward, and each one renders as its own button at the end of the dialogue. Composing rather than configuring keeps the variable surface for each addon focused on just that addon's behaviour.

Techy stuff

  • Built on Cobblemon's dialogue, behaviour, and Molang systems, configured in JSON.
  • Each addon is a standalone behaviour file declaring add_variables. The "compatible with dialogue X" wiring lives in the dialogue init scripts, which check q.npc.config.optional_<addon>_enabled and call the addon's can_offer_dialogue_* script before adding the corresponding option.
  • Permission writes go through a shared cobblemon:give_perm helper. Placeholder substitution uses cobblemon:replace_placeholders and cobblemon:replace_placeholders_var.
  • The {{npc}} placeholder in perm defaults resolves to the NPC's identifier at runtime. As such, the same behaviour config produces unique perms per NPC instance without the server owner having to type them by hand.

Built for the Callisto and Cobblewilds Cobblemon servers, but available for commission.

skin

Pinkan Weedle Line

Regional variants of Weedle, Kakuna, and Beedrill from Pinkan Island.

Originally made for the Fall 2025 Pinkan Island refresh on Roanoke Diamond.

skin

Pinkan Poliwag Line

Regional variants of Poliwag, Poliwhirl, Poliwrath, and Politoed from Pinkan Island.

Originally made for the Fall 2025 Pinkan Island refresh on Roanoke Diamond.

skin

Pinkan Pidgey Line

Regional variants of Pidgey, Pidgeotto, and Pidgeot from Pinkan Island.

Originally made for the Fall 2025 Pinkan Island refresh on Roanoke Diamond.

skin

Pinkan Caterpie Line

Regional variants of Caterpie, Metapod, and Butterfree from Pinkan Island.

Originally made for the Fall 2025 Pinkan Island refresh on Roanoke Diamond.

skin

Painted Mankey Line

Messy variants of Mankey, Primeape, and Annihilape that painted themselved to look more threatening.

Originally made Roanoke Diamond custom starters.

skin

Thunderbird Zapdos

A mythological variant of Zapdos inspired by the great Thunderbird of legend, with sweeping storm-dark plumage.

Originally made for the Summer 2025 Texture Jam on Roanoke Diamond.

skin

Pride Ho-Oh

A radiant variant of Ho-Oh with a iridescent plumage and bright rainbow feathers.

Originally made for the Pride 2025 event on Roanoke Diamond.

skin

Atlantid Relicanth

An ancient deep-sea variant of Relicanth with arcane mechanical markings.

Originally made for the Spring 2025 Texture Jam on Roanoke Diamond.

project6

Crypt Raider Level

This project follows up on the previous: Unreal Engine 5 - Rocky Forest Environment. While designing that level I was focusing on tooling for landscape and foliage design. In contrast, this project was to design an almost entirely indoors and man-made location.

This level was created while following the gamedev.tv course Unreal 5.0 C++ Developer. I used a modular dungeon asset pack to build out the dungeon and crypt, and used a collection of boulder models to construct the cave interior. The level has interactable game elements in the form of objects you can pick up and move around. These grabbable objects are used in two puzzles: unlocking the secret spiral stair entrance to the cave, and leaving the cave crypt with the Holy Grail without triggering a trap!

I'm still finishing up the above course, and will likely have more UE5 projects to post here. Stay tuned!

project4

Unreal Engine 5 - Rocky Forest Environment

The release of Unreal Engine 5 was momentous. With the dynamic LODs provided by Nanite and the automatic shadows/AO generated by Lumen, level design is easier than it's ever been. Inspired by these new tools, I set out to learn the basics of environment design in UE5.

This level was created while following the gamedev.tv course Unreal Environment Design. I started with a basic landscape brush pass, then blocked out the playable area using cliffs and rocks available on Quixel Mixer. Once the landscape and boundaries were in place, trees and foliage were placed and painted around the scene. Finally, a few unique assets and decals were added to give the centerpiece boulder more intrigue.

I'm really excited at the new possibilities UE5 brings. Look forward to similar projects posted here!

project

simpleterm

simpleterm is a bespoke fake terminal written in pure Rust. It lets you create a window with specified size, font, and colors, then display text in that window and get input from the user.

Each call to the terminal returns execution once some condition is met, so you can easily write a complex script of interactions with the user. The terminal functions even include methods to update the terminal's size, colors, and font on the fly!

simpleterm window screenshot

I told you to expect more Rust here — not a month later and I've got a second public crate! I started this almost immediately after finishing intfic after growing frustrated with the lacking support for terminal colors on Windows. By displaying my story in a custom window, I could ensure that it looked relatively the same across all operating systems.

So, in the interest of modularity I set out to make a fake terminal that did a few things very well, and I think I succeeded with those goals in simpleterm! I used piston_window as the backend and did run into some trouble with the way they process fonts and characters. For example, trying to load two sets of glyphs will crash the program unless you reload them right before use! I ended up only loading one at a time, and adding some code to toggle whether the terminal is in "text" mode or "art" mode.

Now that simpleterm is done, I'll be looking to incorporate it with intfic to create intfic_window, so stay tuned!

project

intfic

intfic is a framework that allows you to write a branching story with minimal code. It was made using pure Rust and uses a custom Story File Markup specification.

This was my first public Rust crate, and the first open-source project I've released in general! It came after months of learning Rust online with the official book.

intfic terminal screenshot

To test my skills after completing the book, I set out to create a short narrative game where the player would run away from home and explore a mysterious land. Naturally I began to find building the tools to make the game much more interesting than writing the story itself, and intfic was the result! If I ever do get around to writing that story I can do it all with the Story File Markup system, including setting environment variables, having conditional text and options, and linking different files together from anywhere in the story.

This project was a great introduction to Rust's command line and parsing tools, and after everything I can safely say I'm a huge fan of the language. Look forward to many more Rust projects here in the future!

project3

Relative Evil

"Relative Evil" was my submission to Shacknews Jam: Do it IV Shacknews. The theme of this jam was "Be Relatively Evil", and my goal for the game was to base it all around judging the relative "evil" of other people. At the end of the game the player has their own relative "evil" judged based on their choices. The classic trolley problem serves as a perfect framing to explore these comparisons; even more so when you have to decide very quickly!

This game was made in Unity, unlike my previous projects you may find on this site. I'm aiming to become more proficient in both, so expect my next project (a 3D exploration platformer) to also be in Unity! My favorite part of developing Relative Evil was creating a framework to fade sprites, UI Text, and UI Images on a strictly scripted schedule. Here I used it to show messages between each new scenario, allowing some of the elements to linger slightly longer for dramatic effect. I'll definitely be using the "fadeable" class I developed in other Unity games!

The art was also quite fun to create for this game. It was done entirely in Aseprite. I first created a perfect version of each sprite and then erased parts of the edges by hand different ways in different frames to give an illusion of lively movement. In combination with moving elements such as the eyes of victims and the wheel rod of the train cars this enhanced the game's style quite a bit without becoming unmanageable for a solo dev.

The music and sound FX for Relative Evil were produced entirely by Chris "Groovetone" Habeeb-Louks. He recorded himself playing trumpet and a variety of percussion instruments to achieve the unique jazzy feel of the game. Chris also produced the music for my previous game relation❤ships.

project1

Story Trigger

"Story Trigger" was my submission to the Shacknews Jam: The Third, which had the theme "_____ with Guns". It's a short narrative experience framed like a D&D scenario where the player character somehow has a gun, inevitably messing up the dungeon master's plans quite a lot! I'll try not to spoil much but I urge you to check it out before reading on.

The game was made in UE4 using the FPS starter content. This time around I only made minimal adjustments to gameplay in favor of crafting a complete story. I began with an in-world screen that would display text and options similar to the terminal windows these sorts of games were first made on, and from there it made sense to construct a movie theatre environment for the player to move around in. The player interacts with the story by aiming at and shooting the option they wish to select.

Story Trigger centers around an unnamed character that either does not remember or does not think of their own past. The town merchant and nearby king both seem familiar with you, with the latter distinctly implying that you have saved the kingdom before. The main difference this time is that you, the player, have a gun. Nobody else in the world has ever heard of such a thing and many assume it is magical in nature. Unless told beforehand most would not even recognize it as a weapon. The result is that you are completely overpowered in your interactions with other people. What will you do when you always have the option to use unrivaled deadly force?

Back to the real world now, this is my favorite creation thus far. I've always been more gameplay and mechanics focused in game jams, for obvious reasons. With only a few days to make a game I tend towards finding something quick and satisfying to play, then polishing that with any remaining time. Crafting a complete story was really my only focus for this one. It was certainly more work than I had initially expected, due in large part to the branching narrative I was trying to achieve. For every decision I wanted the player to be able to make I also wanted some acknowledgement later on in the story, causing many seemingly simple interactions to turn into a mess of conditionals behind the scenes. The end result felt incredibly satisfying to me though, especially for the reactions of players as they experienced different parts of my story.

Another reason this game felt much more complete to me was the music by Melliflox. The main theme is quiet and relaxing, but transitions to a darker and glitchy version when the player shoots a character. I can't talk too much about the other tracks for fear of spoilers, but they contributed a ton to the vibe I was going for in my story and several players complimented the soundtrack specifically! Playing your game with music for the first time is often when it clicks and feels like a real thing you made, at least for me. I hope you try out the game, and please feel free to leave a comment or shoot me an email with your thoughts!

project3

Dizzy Dreadnoughts

"Dizzy Dreadnoughts" was my submission to the 2 Button Jam. The core requirement of this jam was that the game must only accept two inputs. You can check out the game with the link below, or read on for some more info on how it was made!

Like my last game jam I used Unreal Engine 4 to make Dizzy Dreadnoughts. All pixel art animations were created in Aseprite and imported to UE4, where I sliced them into Paper2D sprites and set those up in Flipbook. To create the grid of tiles I simply aligned flipbook actors with the appropriate sprites in a grid formation. Every sprite was 32x32 to ensure proper pixel density and to allow for the tight control scheme.

The music was originally created by Joshua McLean (who was running the jam) and was available to all participants. I used one of his songs as-is and remixed the other two with Audacity to make my gameplay loop music. All other sound effects were generated using BFXR.

I learned a ton about pixel art animation and making 2D games in Unreal Engine. Please check out the game above and some screenshots below!

project2

relation❤ships

"relation❤ships" was my submission to the 2019 Winter ue4jam. The theme of the jam was "All's fair in love and war".

The game features a heart-shaped spaceship that can shoot enemy heart-ships to destroy them, or shoot a special projectile to "bond" with them instead. Once bonded the heart-ship will roam around you and attempt to protect you from other enemy ships. You can form a new bond at any time, but watch out for your ex!

I had a blast making this game. Please check it out!

https://ortsac.itch.io/relation-ships

project6

Lan War 35

Twice a year, the IU Gaming club holds a Lan War. STC (my department) sets up a lounge space nearby the main LAN hall. The "STC ARCADE" is aimed to be a nice short break from the intense PC gaming going on.

This time we iterated on the arcade emulator machines, adding a stacked monitor for a much better title screen. We also featured a new 3D printer, some Halo Xbox action, and old horror movies playing on the large-screen TV.

Below is a gallery of images from the event, as well as a poster and wallpaper I designed!

As a bonus, here are the static HTML pages I made to serve as title screens for the arcade machines. View in full-screen for the intended effect:

post

PowerShell - Setting Default Wallpaper

Setting the default wallpaper in Windows 10 using PowerShell seems pretty straightforward at first, but there are a few roadblocks to look out for.

Say you manage a large collection of machines, and need to set different wallpapers based on location, department, or even monitor size? Sure you could use GPO, setting different wallpapers for different OUs, but what if you want different wallpapers on machines in the same OU? In my experience, you can achieve much more granular control using PowerShell.

For instance, the following helper function will return true if the machine's wallpaper is "wide", which we define as having a width that is 2.2 or more times the height.

Detecting Wide-Screen Monitors

function wide_screen? {
    $vid_con = (Get-WmiObject -Class Win32_VideoController)
    $screen_info = $vid_con.VideoModeDescription -split "[^0-9]+"
    $screen_width, $screen_height = $screen_info | Select-Object -First 2
    $screen_ratio = $screen_width / $screen_height
    return $screen_ratio -ge 2.2
}

In my own collection machines have names containing their department and location info, and we save all that data in environment variables. Using these variables we determine exactly which wallpaper path to use. I say path because Windows 10 is tricky with the default wallpaper. If the monitor is a non-standard (1080p) resolution, then good old img0.jpg is not the default wallpaper. Instead an image most closely matching the resolution of the monitor in C:\Windows\Web\4k\Wallpaper\Windows will be used.

To ensure your custom wallpaper displays well on all monitors, you should generate versions of it at these named resolutions:

  1. img0_768x1024.jpg
  2. img0_768x1366.jpg
  3. img0_1024x768.jpg
  4. img0_1200x1920.jpg
  5. img0_1366x768.jpg
  6. img0_1600x2560.jpg
  7. img0_2160x3840.jpg
  8. img0_2560x1600.jpg
  9. img0_3840x2160.jpg

Then place those in a folder called 4k alongside your 1080p img0.jpg. The path to this collection of wallpapers (hopefully on a server!) is what we'll give to the following function:

Setting the wallpaper

function set_wallpaper($new_wallpaper_path) {
    $sysdata = 'C:\ProgramData\Microsoft\Windows\SystemData'
    $old_wallpaper = 'C:\Windows\Web\Wallpaper\Windows\img0.jpg'
    $old_4k = 'C:\Windows\Web\4k\Wallpaper\Windows'

    $new_wallpaper = "$new_wallpaper_path\img0.jpg"
    $new_4k = "$new_wallpaper_path\4k"

    if ((Get-FileHash $new_wallpaper).hash -ne (Get-FileHash $old_wallpaper).hash) {
        "`n  Removing old wallpaper..."
        get_ownership $sysdata
        make_old $sysdata
        make_old $old_wallpaper
        make_old $old_4k

        "`n  Setting wallpaper to $new_wallpaper..."
        Copy-Item $new_wallpaper $old_wallpaper -Force
        if (Test-Path $new_4k) { robocopy $new_4k $old_4k /E /XO /R:3 }
        "`n  Done!"
    } else {
        "`n  $new_wallpaper is already set as our wallpaper"
    }
}

So what is this doing? Let's break it down.

First off, I save variables for various system paths. Some of these we only use once, but saving them to a variable helps keep the code shorter and more readable.

The if statement checks to see if the current 1080p wallpaper is the same file as the new one we are trying to set, using hash. If the hashes match we go to the else statement and simply log that nothing was changed.

If the hashes don't match, we must be setting a new wallpaper. Here many encounter another roadblock in the form of SystemData. Systemdata, located at C:\ProgramData\Microsoft\Windows\SystemData, contains a cached version of the current wallpaper. This cached version is used on the lockscreen of the machine, and if you set a new wallpaper without also clearing SystemData, you'll have the old wallpaper on the lockscreen still!

To clear out SystemData I first call a helper to take ownership of it and grant full permissions for Administrators.

Get Ownership

function get_ownership($dir_path) {
    $dir_owner = (Get-Item $dir_path).GetAccessControl().Owner
    if (!($dir_owner -like "*Administrators")) { takeown /f $dir_path /a /r /d y }
    icacls $dir_path /q /t /c /grant 'Administrators:(OI)(CI)F'
    "`n  Granted rights on $dir_path"
}

Once that's taken care of it's simply a matter of renaming the old sysdata and wallpaper files to make way for the new ones. These 'backups' will only be as old as the last time you changed the wallpaper though, so it's a good idea to save the default Windows wallpapers before you set it to a custom one in the first place!

The regex used in my -replace statement is simply placing _old at the end of the file name while retaining the file extension after. It's a neat trick!

Save Old Version

function make_old($path_in) {
    if ((Get-Item $path_in) -is [System.IO.DirectoryInfo]) {
        $new_path = "$($path_in)_old"
        if (!(Test-Path $new_path)) { New-Item $new_path -type directory }
        Move-Item -Path "$path_in\*" -Destination $new_path -Force
    } else {
        $new_path = $path_in -replace '\.(...)$', '_old.$1'
        Move-Item -Path $path_in -Destination $new_path -Force
    }
}

I hope you found this article useful. If you encounter any problems with the code above or if it helped make your life a little easier, feel free to reach out!

project10

Vectra City / Video Vortex

Vectra City is a class based deck-building game for two players. It's set in a neo-noir world where the shattered sun is perpetually setting on the horizon, constantly shifting colors.

I was a co-founder of the game along with Mitch Ryckman. Mitch was the head designer of game mechanics and balance, while I provided game design ideas and all the graphic design work needed to attract publishers.

In August 2018 we took our work to GenCon, with playtesting sessions and a publisher "speed-dating" event lined up. In preparation for this we commissioned an original piece by the extremely talented Ed Mattinian, which I used in our sell sheet and several other promotional materials.

Here's some of my graphic design work for Vectra City:

Note: the character art in these is all placeholder, except for our commissioned piece.

GenCon was a massive success! We got tons of helpful info from playtesters, but more importantly we struck a very generous publishing deal with Mondo Games. The game was released as Video Vortex in Q1 2020! Official BGG page here.

project4

Lan War 34

Twice a year, the IU Gaming club holds a Lan War. The past few times, my department has sponsored the event by providing technical assistance, 3D-printed trophies for the tournaments, and by being in charge of the lounge near the main hall.

Our goal with the lounge is to provide a space where gamers tired of sitting at their own machine in the crowded main hall can come to relax, or try some novel play experience. This latest Lan War I think we achieved this exceedingly well. The theme was an arcade from the 80's, complete with classic games such as Teenage Mutant Ninja Turtles and Joust, a modern version of PacMan, and a newer arcade FPS called Devil Daggers.

The other main experience involved two immersive driving simulator setups, with triple curved monitors around the user and a steering wheel/pedal combination to control. The game of choice for this experience was WreckFest, a racing game featuring cockpit-view and realistic car damage.

We also sponsored a Hearthstone Fireside gathering for the second time, allowing any nearby players to try a new gamemode and unlock the warlock hero Nemsy Necrofizzle. Additionally, Mitch and I had a playtest of Vectra City set up.

To top it all off, one of my coworkers edited together 80's movies with iconic 80's commercials and music videos between them. The STC Arcade was a huge success, with people coming all night to check out the games, try for a high-score, or relax and watch a classic movie. Below are some pictures from the event space, as well as some graphics I designed to advertise it.

project1

P423 Compilers

My very last class at Indiana University was P423 - Compilers. It was a project course, with the goal of the semester being the construction of a Racket compiler that supported functions.

We broke this difficult task into easier steps, each one translating an intermediary language to something a bit closer to the assembly. Along the way we implemented conditionals, vectors, static and dynamic typed data, and finally user-defined functions.

Our final project was to build on our compiler in a novel or useful way. I chose to focus on optimization of the input code, aiming to produce simpler assembly programs in less time. Check out my final presentation.

project3

Phi Kappa Chi

Phi Kappa Chi was a project I worked on as part of Hoosier Games. It was to be an RPG where you, the protagonist Scotty, are a new pledge for the fraternity PKC. Things go awry when your frat is banned from the campus, and you must conquer the other fraternities to fix things.

The game was heavily inspired by LISA, and would have featured black comedy as a central storytelling element. I served as the project's only developer and was teaching myself Unity as we went.

Ultimately, I think we made good progress for a single semester. The biggest triumph for me was creating a classic RPG battle system from scratch. You can find a video of an older demo in action here and I have some slightly newer screenshots below.

project6

B355 Robotics

I took a basic course on robotics in 2015. We built simple machines with metal and plastic parts, including a microcontroller, gears, motors, servos, and a variety of sensors. We programmed the bots with RobotC.

Labs built up to two main tasks: maze traversal and inverse kinematics PID control. Below you can find a gallery of images I took while building our maze traversal bot. The IK bot can be seen in action here.

project

P317 Code Entropy

I took an Information Theory class back in 2013. Here's a synopsis of my final project.

Entropy is a measure of disorder. In hard sciences, it can be quantified to describe the number of possible states a system can be in. In information theory, entropy can be quantified in a similar way as the amount of information that can be acquired by observing the state of a system. A static system will have entropy zero; an un-weighted coin flip will usually have entropy around 1 bit.

Finding the entropy of natural language has certainly been done before. Any string of words may be treated as a list of characters. In turn, the probability of each character can be measured and the entropy can be found based on Shannon's equation. However, code is not natural language. We may type it character by character, but not every character is considered equal when the computer interprets our program. The largest difference is that words using alpha-numeric characters may be grouped together. As a whole, any word and most punctuation in programming languages can be referred to as a "token". A token is essentially any object that can be interpreted by the computer. It is the smallest possible unit that is significant in code, thus it is the smallest possible unit with which we should measure the entropy in code.

My task became clearer at this point. To find the entropy of a program, I first had to tokenize it and collect a list of all the tokens. Here's the function in charge of making that list:

def tokenize(byte):
    global token
    if byte == ' ' or byte == '\n':
        if token != '':
            tokens.append(token)
        token = ''
    elif byte in string.punctuation:
        if token != '':
            tokens.append(token)
        tokens.append(byte)
        token = ''
    else:
        token += byte

Once I iterated through the entire list, the entropy of the list of tokens could be calculated based on each member's conditional probability. Following is the formula for Shannon Entropy:

def entropy(list):
    entropySum = 0.0
    seen = []
    for x in list:
        if x not in seen:
            probability = list.count(x) / len(list)
            entropySum += probability * log2(probability)
            seen.append(x)
    return -entropySum

Full source code can be found here.

It is interesting to note the varying effects that tokenizing a string can have on its entropy. For instance, if one takes the entropy of a relatively simple program without tokenizing it and compares that to the tokenized entropy, it's likely that the tokenized entropy will be lower. This is because a tokenized string will have less members than one in which every character is considered independent. However, the more complex a program gets, the greater the tokenized entropy will be, eventually far surpassing the un-tokenized entropy. This is due to the number of possible characters being far lesser than the number of possible combinations of those characters. This allows for entropy to follow vocabulary linearly in very long or complex code, whereas normal methods for calculating entropy of a string would follow a more logarithmic pattern as the code grows in complexity.

To compare the entropy of different languages, one should use code that accomplishes the same task. For example, here's the 'reverse string' program in each of the languages I analyzed:

Brainfuck reverse string

,----- ----- [+++++ +++++ > , ----- -----]<[.<]+++++

C++ reverse string

int main() {
    std::string s;
    std::getline(std::cin, s);
    std::reverse(s.begin(), s.end());
    std::cout << s << std::endl;
    return 0;
}

Java reverse string

public static String reverseString(String s) {
    return new StringBuffer(s).reverse().toString();
}

Python reverse string

string[::-1]

Scheme reverse string

(define (string-reverse s)
  (list->string (reverse (string->list s))))

Haskell reverse string

reverse = foldl (flip (:)) []

I chose these languages to represent a broad spectrum of programming languages, but if research is to be taken further it may be wise to increase the scope of the languages analyzed. My general hypothesis followed that the languages could be ordered according to ascending complexity/entropy: Brainfuck, Scheme, Haskell, Python, Java, C++. To test this, I analyzed code from each language across several tasks. I used Rosetta Code as a database to find standardized examples of different tasks in each programming language.

Results

| | Hello World | Reverse String | Bubble Sort | |:--------- | -----------:| --------------:| -----------:| | Brainfuck | 1.85 | 2.05 | 2.59 | | C++ | 4.28 | 4.40 | 5.48 | | Haskell | 2.95 | 3.10 | 4.23 | | Java | 4.50 | 3.76 | 4.80 | | Python | 2.95 | 2.52 | 4.39 | | Scheme | 2.25 | 3.01 | 3.59 |

The results roughly supported my hypothesis, with Brainfuck and Scheme being less complex, C++ and Java being more complex, and Python and Haskell sitting around the middle. I'd be interested to see if this trend would continue when vast amounts of code are analyzed, including standard libraries.

And there you have it, my little foray into Information Theory. It was one of my favorite classes so I hope you found this project interesting!