Blog


Turning the Junior Ranger wireframes into a working prototype

After the Challenge List and wireframes, the next step for Junior Ranger AI Mini Challenges was making it clickable: an actual web app children could tap through.

Web, not an app store. It’s built as a web app rather than a native app, so families can open it in a browser on a phone or tablet without installing anything. The wireframes were already drawn with browser chrome for this reason.

The stack. I’m building the front end in Expo with TypeScript, following the architecture decisions and privacy rules the team had already agreed on.

The screens:

  • Home and an All Challenges browse screen
  • Challenge detail, capture and result, the core loop of picking a challenge, taking a photo and getting feedback
  • Collection, grouped by species type (bugs, birds and so on) and showing what you’ve found and what’s still locked
  • Profile and settings, stored in the browser with no account or login
  • Accessibility, with a text-to-speech toggle and room for more options

Accessibility checks. The designs aim for low text and read-aloud support, and I checked colour contrast and touch-target sizes against WCAG 2.2 AA. For younger kids on small screens, big targets and clear contrast matter as much as the content.

Tests. I added automated Jest tests so changes don’t quietly break screens the team is relying on.

Having the challenges written first paid off again here: every screen had real content from day one instead of placeholder text.

Polygon budgets: what Overwatch, WoW and RuneScape taught me

Before making characters for my RPG, I wanted to know how many triangles a character should actually have. Here’s what I found. Some figures are confirmed and some are only estimates, and I’ve kept them separate.

Confirmed or well sourced:

  • Old School RuneScape: around 500 triangles per character. Proof that style matters more than polycount.
  • Overwatch: a Blizzard tech artist has confirmed about 30,000 triangles per hero plus 15,000 for weapons, with 4 LOD levels that each halve the count.

Estimates (no official numbers):

  • Classic WoW models are reportedly roughly 1,000 to 6,000 triangles, with modern ones much higher.
  • Valorant, CS:GO and RuneScape: Dragonwilds don’t publish numbers. Comparable games suggest anywhere from 10,000 to 40,000.

What I’m taking from this:

  1. Pick a budget per art style, not per “quality”. A stylised game can look great at a fraction of the triangles.
  2. Budget body and weapons separately. Overwatch’s split makes sense when characters can swap gear.
  3. Plan LODs from the start. Halving at each LOD level is simple and scales well when lots of players are on screen, which matters for a WoW-style game.

My plan is a fixed body budget, a separate weapon budget and halving LOD levels, then testing in Unity to see where performance actually starts to drop.

Can a 6GB GPU make game assets with AI?

I’m working on a WoW-style RPG in Unity: multiple races and classes, a skill tree, and combat that keeps moving. That needs a lot of 3D assets, so I looked into AI text-to-3D tools.

The short answer for my PC: not locally. I’m on an RTX 2060 with 6GB of VRAM.

  • Hunyuan3D 2.1 needs around 10GB just for shapes and over 20GB with texturing.
  • TRELLIS wants a 16GB+ card.
  • Lighter options like Hunyuan3D-2mini or TripoSR might run, but expect rough, untextured results and a lot of fiddling.

What I’m doing instead:

  1. Hunyuan3D’s official web version for props like weapons, crates and trees. It’s free, with daily generations at full quality.
  2. Meshy’s free tier for characters, because it can auto-rig, which matters when you need many races to test.
  3. Text-to-image first, then image-to-3D, which gives more control over the style than prompting for 3D directly.

Whatever the tool, the output is a starting point. Expect to fix topology and UVs before anything is game-ready.

As a test, I also had a simple low-poly human generated with a Python script: about 530 triangles, 1.8m tall, exported as GLB and OBJ so it drops straight into Unity at the right scale. It’s a base mesh for blocking out characters, not final art.

If I upgrade, a 16GB card would open up local generation. For now, cloud tools it is.

Starting my RPG

I’ve started work on an RPG in Unity, inspired by World of Warcraft. Here’s what I’m aiming for.

Races and classes. Players pick from multiple playable races and classes, each with its own abilities. I want the choice to feel meaningful from the start, not just cosmetic.

A skill tree. Characters grow through a skill tree, so two players of the same class can still end up playing differently.

Combat that keeps moving. Combat mixes melee, ranged and magic. The main goal is for it to stay fluid while you’re on the move, instead of locking you in place every time you attack or cast.

The camera. The camera sits slightly behind and above your character in third person, and you can zoom all the way into first person, the way WoW does. Some players like seeing their character and some like being in the action, so both should work.

I’ll post updates here as it takes shape.

· WoW: Forever Addons

Porting an addon to WoW Forever, and why I stopped

When WoW: Forever (Blizzard’s Vanilla-based “Classic+” mode) went into beta, I wanted to bring my swing timer over.

The problem: there was very little reliable information about the API. A lot of what turned up in searches was low-quality or just wrong.

The fix: look at something that already works. I grabbed an existing addon that supports Forever and checked its .toc file. That confirmed the Interface number (16001) and showed that Forever uses the Classic Era API, so none of the Retail-only C_ namespaces are available.

With that confirmed, the swing timer was ported with:

  • separate melee and ranged bars
  • Auto Shot and Shoot resets matched by spell ID
  • dodge-haste handling
  • detection of on-next-attack abilities like Heroic Strike and Raptor Strike

One bug along the way: Classic Era reads combat log data through CombatLogGetCurrentEventInfo() instead of the older positional arguments, so the combat log handling had to change.

Why I stopped. It turned out WoW: Forever already includes this in the base game, so there was no point maintaining my own version. I shelved it.

Two lessons from this one: when docs are missing, a working addon’s files tell you more than search results, and it’s worth checking what the game already offers before building something.

· Etsky's Combat Addon

Mobs don't leash by distance, they leash by time

After the swing timer, I wanted a warning before a mob I’m kiting resets and runs home. My assumption was that leashing is about distance: pull a mob too far and it gives up. So the first version of LeashTracker measured how far the target was from where the fight started.

It didn’t line up with what happened in game. Looking into how AzerothCore handles it showed why: the reset is mainly based on time. If no damage is exchanged in either direction for roughly 10 to 12 seconds, the mob evades and resets. There’s also a small inner radius where mobs never reset.

So LeashTracker was rebuilt around the right thing:

  • It tracks the time since the last damage exchange, not distance.
  • It learns per-mob timeouts by watching for the “Evade” aura being applied in the combat log.

Putting it together. By then I had a few separate addons, so they were merged into one: Etsky’s Combat Addon. It has:

  • the melee and ranged swing timer
  • LeashTracker
  • a resource bar for Energy, Mana, Rage and Runic Power, with a highlight near cap and a rough Five-Second-Rule indicator for mana users

A /ecom menu toggles each module and handles unlocking, locking and resetting bar positions. All debug commands were stripped out of the final version.

Lesson learned: check how the server actually implements a mechanic before building UI around how you think it works.

· Etsky's Combat Addon

Why my ranged swing timer drifted

I play on a Wrath of the Lich King (3.3.5a) private server and wanted my swing timer to track ranged attacks as well as melee: Hunter Auto Shot and wand Shoot. It turned into a good lesson in not trusting your first approach. I built it with an AI coding assistant, and my job was testing every version in-game and reporting back what broke.

Problem 1: getting ranged speed. Melee is easy, because UnitAttackSpeed("player") gives you the swing speed directly. For ranged, the first plan was to read the weapon’s tooltip and pull out the “Speed” line. On this server the tooltip doesn’t have a Speed line at all. It goes name, ranged, damage, DPS, item level, and that’s it.

The fix came from a reference addon: the first value returned by UnitRangedDamage("player") is the ranged attack speed. No tooltip scanning needed.

Problem 2: drift. The timer originally reset on the RANGE_DAMAGE and RANGE_MISSED combat log events. Those fire when the shot lands, not when it’s fired, so on long shots the bar slowly fell out of sync with my actual attacks.

Switching to UNIT_SPELLCAST_SUCCEEDED and matching the spell name (“Auto Shot” or “Shoot”) fixed it. That event fires the moment the shot goes off, so projectile travel time no longer matters.

Other 3.3.5a gotchas. StatusBar:SetReverseFill doesn’t exist on this client, so the bars use a texture that’s anchored and resized manually instead. The original version also depended on a shared library that wouldn’t load, so I had it rebuilt as a standalone addon with no dependencies.

The takeaway: on old clients, assume nothing in modern addon tutorials exists until you’ve tested it in-game.

Rebuilding loxton.dev

My old site was hard to update, so I rebuilt it with two goals: show my projects properly, and make posting so easy that I actually do it.

How it works now:

  • Astro builds the whole site as static pages.
  • Projects live in one data file. Each project gets its own page automatically.
  • Blog posts are Markdown files. Instead of separate pages, they render inline on one blog feed, paginated as it grows.
  • Posts can attach to a project. Tag a post with a project and it shows up as an update on that project’s page, so each project gets a running log.
  • An admin panel lets me write and publish posts from the browser. Saving commits to GitHub and the site rebuilds itself.

Hosting. The site started on GitHub Pages with my domain pointed at it. I moved hosting to Vercel, which handles the build on every push and also runs the small login endpoint the admin panel needs.

From here, this blog is where I’ll post progress on game projects, addons and cyber security work.

Most of machine learning is cleaning the data

For a uni machine learning project, we predicted whether a driver would accept a coupon offer, using a dataset of 12,684 driving scenarios with 26 variables. I owned data preparation and the logistic regression model. Data prep took far longer than the modelling.

What the data threw at me:

  • A column that was almost empty. One variable was missing in about 99% of rows. No imputation rescues that, so it went.
  • Small gaps worth filling. Several “how often do you visit X” columns were missing 1 to 2% of values. Those were worth imputing rather than throwing rows away.
  • Numbers that aren’t numbers. Age mixed values like 21 and 26 with text like below21 and 50plus. Time was stored as strings like 10AM. Both needed converting before a model could use them.
  • Variables that repeat each other. Some binary columns were very highly correlated. A correlation heatmap made those easy to find and drop.

Then the models. I built and tuned the logistic regression with GridSearchCV, and my teammate Luke built the decision tree and neural network. For my individual part, I compared all three saved models on a fresh test split using accuracy, ROC curves and AUC. The neural network came out on top.

What I’d tell myself at the start: read every column before touching a model. Classifying each variable as binary, ordinal, nominal or numeric up front saved me from encoding mistakes later.

Writing outdoor challenges for 9-year-olds

Junior Ranger AI is an industry project I’m working on at uni for the Queensland Indigenous Land and Sea Ranger Program. Children in Years 4 to 7 complete short environmental challenges between Ranger visits: they photograph or name a species or item and get simple AI-assisted feedback.

My first big deliverable was the Challenge List: the actual challenges the app offers. It’s harder than it sounds.

What ended up in it:

  • 23 challenges across four categories: Wildlife, Plants, Recyclables and Environmental Features.
  • Instructions written at a Year 3 reading level, below the youngest users, so nobody gets stuck on the words.
  • Content for northern Australia: regional species and wet and dry season framing, not generic examples.
  • Safety notes kept in red, like keeping away from water’s edge because of crocodiles and not handling cane toads. Normally I’d avoid relying on colour, but here it’s a deliberate safety signal.
  • Plant challenges go to a specialist plant-recognition service, because general image recognition is weaker on plants.

Open questions. Not everything resolves cleanly. One challenge involving cane toads is still being checked against the “safe species” rule because of their toxicity. Another question is how children without a working camera can still take part.

After the list came the wireframes in draw.io and Figma: home, challenge, capture, result, a species collection screen grouped by type (bugs, birds and so on), and an accessibility screen with text-to-speech. Writing the content first made the screens much easier to design, because I knew exactly what each one had to hold.

Designing a 2v2 map around two chokepoints

I’ve been playing a lot of Beyond All Reason, so I started planning my own map for it.

Size first. 16x16 is the usual size for 2v2, but I went with 14x14. A slightly tighter map means earlier contact between players and less time sitting back and expanding. In BAR the heightmap size comes from the map size (64 × size + 1), so 14x14 needs an 897×897 heightmap.

Layout. I started from a reference minimap and went through a few sketches before I was happy:

  • Each team gets two corner starts on its own side (west vs east).
  • Four mountain masses sit around the middle with a big central peak among them.
  • Only two open routes cross the map: a north pass and a south pass.

One early version had two rivers crossing in an X through the centre. It looked interesting, but it muddied which side was which and didn’t give me the clean chokepoints I wanted, so I went back to the four-mountain layout.

Why two passes? With only two ways across, holding a pass matters, and a team can fake pressure on one side while committing to the other. Flanking becomes a decision instead of something that just happens.

What’s left. A playable BAR map is more than a heightmap. It also needs metal and grass maps, textures, a mapinfo.lua config, and everything packed into an .sd7 archive. I got as far as the layout and the heightmap plan, and the project is shelved for now. If I pick it back up, the next step is generating the heightmap and placing metal spots.

· Buzz

Design patterns, load balancers and a white screen

Buzz is a discussion forum my team of three built for a uni software unit: React on the front end, Node.js and Express on the back end, and MongoDB.

Patterns that earned their place. I built admin announcements. An admin post is pinned to the top, shown with a red outline, and sends a notification to every user. A Factory decides whether a new post is a regular post or an admin post, so the controller never has to care which one it’s handling. Notifications use an Observer, so creating a post triggers notifications without the post code knowing how they’re sent. Before this project, design patterns felt like exam material. Here they kept the code readable.

I also added likes, comments with GIF support (Giphy API) and a sort toggle for the feed.

Testing. I wrote backend functional tests with Mocha, Chai and Sinon. The hardest part was stubbing the database properly so tests didn’t depend on a live connection. It made the difference between unit tests and integration tests click for me.

Deployment pain. We deployed to two AWS EC2 instances behind an Application Load Balancer, with nginx, PM2 and a GitHub Actions pipeline. It did not go smoothly:

  • One instance was set up wrong and showed as unhealthy, so the load balancer barely sent it traffic. We rebuilt it.
  • Every reset brought new problems: permissions, missing files, the frontend not rendering.
  • An intermittent white screen never fully went away.

Close to the deadline, we decided as a team to stop making changes rather than risk breaking more. Knowing when to stop turned out to be one of the more useful lessons.

What an incident response plan actually needs

In my cyber security governance unit, I did a risk assessment and incident response plan for a real organisation, then defended it in an oral assessment. Here are the ideas that stuck.

Know your recovery targets first. A business impact analysis tells you what matters most and how fast it has to come back:

  • RTO (Recovery Time Objective): how long a system can be down.
  • RPO (Recovery Point Objective): how much data you can afford to lose, measured in time.
  • MTD (Maximum Tolerable Downtime): the point where the outage does real damage.

Without these, you might restore the wrong system first.

Follow a lifecycle. NIST SP 800-61 breaks incident response into four stages:

  1. Preparation: roles, tools, contact lists and practice runs.
  2. Detection and analysis: spotting it, judging how bad it is, writing things down.
  3. Containment, eradication and recovery: stop the spread, remove the cause, restore from clean backups.
  4. Post-incident activity: a lessons-learned review, then update the plan.

Plans go stale. A plan last tested two years ago, before new cloud tools were added, isn’t really a plan. It needs regular review and testing.

“We have backups” isn’t a risk treatment on its own. Backups reduce impact, but only if they’re tested, isolated from the network and recoverable within your RTO. Otherwise the risk rating shouldn’t come down.

Know your legal obligations. In Australia, organisations covered by the Notifiable Data Breaches scheme have to notify the OAIC and affected people when a breach is likely to cause serious harm.

The biggest shift for me: incident response is mostly about decisions made before anything goes wrong.

How SYN cookies stop a SYN flood

For a networks and security project, my group analysed a packet capture of a TCP SYN flood attack on a file server. My part was the mitigations. Here’s the short version.

The attack. A TCP connection starts with a three-way handshake: the client sends SYN, the server replies SYN-ACK and waits, and the client sends ACK. While it waits, the server holds a slot in memory for that half-open connection.

A SYN flood sends huge numbers of SYNs, usually from spoofed addresses, and never finishes the handshake. The server’s queue of half-open connections fills up and real users can’t connect.

Mitigation 1: SYN cookies. Instead of storing state for every SYN, the server encodes the connection details into the sequence number of its SYN-ACK. If a real client replies with an ACK, the server can rebuild the connection from that number. A flood of SYNs no longer fills memory, because nothing is stored until the handshake completes.

  • Where: Layer 4, on the server itself.
  • Limitation: it protects the connection queue, not your bandwidth. A big enough flood can still saturate the link.

Mitigation 2: Ingress filtering (BCP38). Networks drop incoming packets whose source address couldn’t legitimately come from where they arrived. That makes spoofing much harder in the first place.

  • Where: Layer 3, at network edges.
  • Limitation: it only works if networks other than yours deploy it, so you depend on everyone else doing their part.

The two work well together: SYN cookies protect the server now, and ingress filtering reduces spoofed traffic across the internet over time.