PortalWorlds Progress

  • PortalWorlds 0.9 on mysticdungeon.club

  • 2020-03-18:

    • Mob names
    • Boss monsters (only ones with a nametag, and much tougher than the rest of the level)
  • 2020-03-17:
    • Structures: circle tower, box tower, campsite
  • 2020-03-16:
    • All levels are solveable
    • Map indicates map name and row/column of player

Pop back over to itch.io or my Patreon if you want to support this!

What I'm Watching: Kingdom S2

After a brief flashback to the first use of the resurrection flower against the invading Japanese, S2 picks right back up with the zombies out in daylight, and a fantastic retreating battle, and heroic sacrifices winnowing the cast a bit.

There's more straightforward conflict this season, since we know who the villains are (anyone named Cho). A little of what they'd previously done fills in backstory, and desperate measures against the Japanese make sense, but not so much why they're still doing it; ambition, sure, but hitching yourself to the Queen's clan or using the plague as a weapon are not things sane people do.

A cop with an impressive feather hat tries to investigate the Queen, and gets further than I'd expect from feudal investigators, but politics makes that entire subplot pointless. She isn't especially cunning, her plots and tricks are very blunt and obvious, but nobody can call her on it, and her impossibly loyal guards and court ladies go along with it.

The Crown Prince does more swordfighting this season, though mostly it's hacking up zombies instead of duels. The "Tiger Hunter" peasant with a gun amuses me, and he finally gets one backstory flashback, but he's low on dialogue.

Nurse Seo-Bi in any other era would be a boss, with the most valuable medical/murder tool ever and the cure for the zombie plague, but in feudal Korea she's just pushed around as a pawn, and treats entirely the wrong person. Half the plot could be avoided if she just told the Crown Prince what she knows earlier, and doesn't help the villains.

By E3 & E4, I'm really missing the zombies; there's too much Human backstabbing and just chasing around the countryside which is zombie-free except at the surrounded fortress. And some of the death scenes last a very long time, many minutes of weeping and flashbacks.

Finally by E5, we get some zombie action again, but it's taken forever. Zombies vs guy in toilet is always a great set piece. The "camera following unseen action behind a wall" scenes get annoying quick; I prefer to see the combats.

"People aren't screaming. The screams have stopped."
"What's going on?"

The plan in E6 relies on zombies not being able to climb, which eventually they do, an ice lake breaking in a way that it doesn't (ice is bouyant, so breaking it in one place doesn't shatter the entire lake).

The "7 years later" setup for the next season is a little heavy on tell-not-showing, but we have a new villain teaser at least.

I did get bored mid-season. Zombies, ever since Night of the Living Dead, have existed to put pressure on people, and keep a plot advancing fast. Without the zombies, you just have people whining at each other, making too-elaborate plots, and they don't even have to stay in the house/castle. With zombies, you get desperation and quick, bad decisions in enclosed spaces.

★★★★½

How to Work from Home

I've been working from home for years, and I've got a whole process:

  1. It's fine to wear bathrobe (or pajamas) for a while. Naked people get very little done.
  2. Breakfast and coffee time can be extended by playing videogames, watching TV, or reading comics.
  3. Switch to your work environment. If you have a home office, go there; if not, use multiple desktops to at least not work on the same screen as your browser or chat windows. Select appropriate work music.
  4. Try to get at least one item from your task list done. It's OK to take the lowest-hours problem if you think you can knock it out.
  5. Time for a little reward slacking off.
  6. OK, now it's lunchtime. Now you may need to be professional and wear pants/dress/kilt after lunch, or before if you're ordering in or going out. However, unless you go outside there's no reason to change out of slippers.
  7. Spend too long staring at your task list, going "uuuuugh."
  8. Say "good enough for one day!", check in/archive work.
  9. Goofing off time. Possibly time for a nap before dinner.
  10. If you think about the project, and record some tasks, that counts as "working late" and you can take any reward you want.
  11. Whiskey time.

There's all kinds of sanctimonious blowhards who tell you how to "be productive", "wear pants", "always be shipping". Forget them, this is a realistic schedule you can live by.

PortalWorlds Progress

Doing some more work on the post-7DRL version of PortalWorlds, will publish this when judging's done.

  • 2020-03-13
    • Message history, toggle with H.
    • World counter.
    • Terrain smoothing.
    • Rivers.
  • 2020-03-16
    • Longer tutorial level (and first predefined map, may add more of these later).
    • Messages display on screen instead of HTML.
    • Increased view distance/window size. Should make this configurable for small screens.
    • Physical layout for on-screen keyboard.
    • Started on making all worlds solveable.
  • TODO: I have a longer list, but these are Real Soon Now.
    • Finish making worlds solveable.
    • Structures and level bosses.
    • Audio.

Quarantined Monday Music

Raveonettes for real rock 'n roll, some downbeat fuzzy pop, then something dumb and funny to cheer you back up.

Worst Year Ever?

  • 2016: Worst Year Ever? Ha ha no. slate
  • 2017: Worst Year Ever? Maybe. thetylt
  • 2018: Not as bad as 1968, according to Vietnam vet: usatoday But said vet could put himself thru school and buy a house and car on a middle-class job, so his comfortable sanctimony reads like Republican marketing bullshit now.
  • 2019: For once, nobody seriously suggests it's the worst year, except Brexit/Remain people. Everyone else in the world laughs at the English self-destructing.
  • 2020: Strong opening with a plague that may kill 119M+ people (7e9*0.5*0.034) because many governments are unwilling or incapable of testing and quarantining people, but let's see where this party goes. It's not even the Ides of March yet.

Disaster Warning Wednesday Music

Retrospective: PortalWorlds

What worked, what sucked, lessons learned, The More You Know, and knowing is half the battle, go Cobra, etc.

JS: Test Your Libraries -1

Especially the hard-to-test parts. I had one "obviously correct" array shuffle function I've been using for maybe 10 years:

WRONG RIGHT
function arrayShuffle(arr) {
    for (let i = arr.length-1; i >>= 1; --i) {
        const j = Math.floor(Math.random() * i); // WRONG. NO.
        const tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
    return arr;
}
/** Fisher-Yates */
function arrayShuffle(arr) {
    for (let i = arr.length-1; i >= 1; --i) {
        const j = Math.floor(Math.random() * (i+1)); // RIGHT. YES.
        const tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
    return arr;
}

And because of that, I got a distorted shuffle. I only noticed because no mob using my dumb-as-rocks not-even-AI would ever choose to move north… everything ended up piled up at the bottom of the map.

It's hard to test randomness, but you can check that values are in a sane range. For a decade I've had wrong code just copy-pasted into new projects because I never checked my assumptions.

JS: UI Libraries are Great +1

I mean, I knew that. But how easy it is to throw something up is a bit of a surprise every time I do it. It's a fast, dynamic, functional/OOP language that also has good graphics and sound libraries (I didn't do sound in PortalWorlds, but I will before 1.0), and tolerable event handling these days.

I'm not releasing the full source, but in a week or two I'll add a few more things and release my common library under BSD license. This is mostly stuff from my Learn2JS project, and some from TTMS-76, but those have a giant framework for running scripts, this is just a few functions for a pure JS application, and greatly condensed.

Pulling in common code makes this stuff so much easier.

JS: Application Structure -1

There's no proper "run loop" in PortalWorlds, and late in the process that bit me in the ass.

What you should do is, collect events, put them in a queue. Every X milliseconds (I usually use 30 FPS, so 1000/30=33.333ms) have setInterval do an event-update-redraw loop. If something takes a long time or requires animation updates, it must be done in chunks or in a background thread (using Web Workers ). This is true of any application, not just games.

What I did wrong is having events immediately perform actions, and the setInterval just does update-redraw. This is much easier, but it's wrong. I have no control currently over blocking user actions, and animation has to all happen at once.

The trick I used is to keep a "frame" counter, and that chooses animation frames, moves floating text and the tracers of missiles and fireballs. Next turn invoked by user event just wipes those out. An amusing side-effect is missiles vaporize corpses as they fly past.

Switching to a correct run loop isn't super hard, but does require changes to all my timing and animation hacks, so at this point it's not worth it.

JS: Fonts -1

I used Oryx's "simplex" font, which looks fine and bitmappy in web pages, but in Canvas it gets antialiased, and I wasn't able to make it stop. So I have to make all fonts slightly larger than I'd like, and they're still kind of blurry.

The smarter way would be to use a bitmap font, and a very simple bitmap text renderer. That's what I do in Perilar: Dark Weaver, with an ATASCII-inspired fantasy font. But in 7 days I didn't have time to write and test that.

That's on a "maybe later" list for 1.0. Alternately, I could figure out how to get them antialiased in Canvas?

JS: Minimizer -1

I used to use yuicompressor, which renames variables and aggressively minimizes your code into unreadable but very compact line noise. But with the long-drawn-out death of Yahoo!, that hasn't been updated in a decade, and it doesn't handle ES2020.

I could probably have run everything thru Babel and then yuicompressor, but it's time to move on? So I just used Crockford's jsmin which only removes whitespace. It might be a good time to look into compiling JS to WASM binaries.

My build.zsh script:

#!/bin/zsh
rm -rf portalworlds*.zip
rm -rf build
mkdir -p build
mkdir -p build/js

# *.js -> .min.js
for f in js/*; do
    n=`basename $f .js`
    perl -ne 's/const DEBUG = true;/const DEBUG = false;/; s/^\s*DLOG.*$//; print;' $f >build/$f
    jsmin-crockford <build/$f >build/js/$n.min.js "Copyright (c) 2020 by Mark Damon Hughes. All Rights Reserved."
    rm build/$f
done

# index.html
perl -ne "s/\.js'/.min.js'/; print;" index.html >build/index.html

cp -R favicon.* style.css i ttf build
cd build
zname=portalworlds-$(date "+%Y%m%d_%H%M%S").zip
zip -r9Xq ../$zname * -x "*.DS_Store"
cd ..
echo $zname

Game: Combat Design +1

So I knew I wanted combat to be very swingy (allowing anything from instant death to instant kill), but not have levels or many stats; I didn't have time or inclination to make another detailed RPG!

Instead, combat works by adding your current strength and enemy's current strength, rolling 2 dice in half that range, so central results are more likely, but it's easy to get results at either far end. Then apply the difference from your strength as damage to you or the monster, depending on which side of the line it's on:

const roll = Math.floor(dice(2, this.strength + mob.strength)/2);
if (roll <= this.strength) {
    const dmg = Math.floor( (this.strength - roll) * (100 + this.getDamage()) / 100 );
    mob.takeDamage(dmg, true);
} else {
    const dmg = Math.floor( (roll - this.strength) * (100 - this.getDefense()) / 100 );
    message(mob.toString()+" hits you for "+dmg+" damage");
    this.takeDamage(dmg);
}

Kind of ridiculously simple, but it makes for symmetric combat, so player-attacks-monster and monster-attacks-player have the same results. It's not practical for a tabletop game, but this is a really fun mechanic in a computer game.

Damage and Defense from gear just modify the result by percentiles, they don't affect attack roll at all. If your strength + defense % is less than the monster's strength, you can be one-hit-killed, and vice versa.

Experience adds to current strength, and increases base strength if you're near max; you need to finish fights against slightly superior foes unharmed to grind up strength.

Game: Spells +1/-1

I knew I didn't have time for a lot of magic, and I never feel like I'm finished with magic systems anyway. So here I just picked 4 types: AOE damage, AOE control, escape, and heal; or as the game knows them, Fireball, Sleep, Invisible, and Heal. Then rather than have MP and worry about regeneration, I just give you "base" spells in each based on class, and you can pick up scrolls to add new points to current spell total. When you heal with mana potions or going thru a portal, you get back your base spells.

Last three were trivial: Sleep just searches a radius and has a die(100) > strength chance to give humanoid or animal targets a sleep condition for 2d4 turns; if they're sleeping, they skip the turn. Invis just sets an invis condition for 2d4 turns; mobs ignore you if you have that condition. Couple places like melee and taking damage I manually clear the Invis and Sleep conditions. Heal just heals 50% of base strength, woo.

Even with just 4, doing Fireball turned out to be quite challenging (the Application Structure problem); how do I show it go across the board, maybe have a turn or two delay, then explode? Well, it does it by moving 4 steps every turn, and only having the fake animation for a tracer of where it's been.

Game: Permadeath +0

Permadeath happened more by lack of save state than any intention. I much prefer to have save slots, and then you can choose to save your game and reload last or earlier, or you can play hardcore and never reload. It puts the moral burden of choosing permadeath on the player, not the developer.

But JS localStorage doesn't really have room to stash a huge amount of data. In Reaper's Crypt I had a very aggressive compression for the map (which looks like a giant grid of tiles but it's really about 64 rooms per level), and it's still problematic, sometimes maps just can't be saved.

I could save the character as of last portal you entered, and regenerate maps. But that encourages "stair-scumming", which I do have a problem with. So instead this is very arcade-like, you just play until you die, then insert another quarter.

I do plan to add a scoreboard, both local and maybe server-based for a copy hosted on mysticdungeon.club.

Personal: Shipping is Awesome +1

Actually finishing something and making it public without a decade-long process is amazing. It's not perfect, and I know nothing is perfect, I should just ship things, but I can't normally do that. Having a hard deadline and meeting it was the best feeling.

Personal: Drone-Slack Balance -1

I was desperately exhausted afterwards. Bone tired all day Sunday, and I'm still feeling it on Monday. 7 days in a row, even with a couple shorter days, is about 3 or 4 days too many.

I'm nocturnal. I like to wake up ideally just before midnight (but sometimes backslide to evening if Real Life interferes), eat and coffee, then work until dawn, walk the dog at sunrise, maybe get my own walk in (tiny dog cannot keep up for a mile+), then finish up and goof off until early afternoon when I can sleep. The schedule weirds out some daywalkers, but it's quieter and more compatible with "morning people" (ugh) than waking in afternoon and sleeping at dawn, which I did for decades.

But normally that work is 3 days of code, 1-2 days of writing or art, 2-3 days of just playing videogames or going Outside, doing Real Life AFK stuff.

Drones who can work all the time frighten me, they're basically Terminators. People who Slack all the time frighten me, they're a waste of precious oxygen & water, and I may foolishly try to rely on these people and get nothing. You need to be in the middle area.