Orkn's Pulp Prototypes

This sounds like Pulp is crashing. If you open your browser console when playing you should see an error that'll help to understand the problem.

My guess is that one of the left facing player tiles is named incorrectly (perhaps you duplicated it and forgot to delete the "2" at the end?) and that's causing an error when the script tries to use a tile that doesn't exist.

If you are atill struggling and can share the error from the browser console, that'll help!

1 Like

I changed all the names to what they were originally and that fixed the problem.

This accident only happened because I got tired of changing every sprite twice for the side facing animation since they were the same.

Thanks for helping can't wait for the next post about your game that your working on!

1 Like

Also am I allowed to use this in future games I may make?

Of course, go for it! Would love to see what you come up with :slight_smile:

1 Like

Lolife progress continues at a good pace! Here are a couple of fun mechanics:

lolife-sheep-herding

These sheep are timid and run away from the player, which means they can be herded up! The outer edge of the area is actually made up of sprites with this script:

on interact do
  goto event.x,event.y
end

To the player they behave no different to non-solid world tiles as the player can pass straight through thanks to the goto in the interact event, but to the entity movement logic, which is using the solid function, the tiles report to be solid sprites and therefore impassable. This means the sheep can't flee right up to the edge of the area, allowing the player to always be able to get behind them to herd them back towards the middle.

lolife-crank-open-portcullis

Elsewhere, here's an example of how I'm using the crank for a hopefully memorable moment. This is just one of several ways you can gain entry to the castle (don't worry, I'm not going to spoil the others!). Behind the scenes there are some item tiles around the winch to track when the player steps next to and away from it. If you're adjacent and have the gate key, you can crank to open the portcullis, which I implemented by adding event.ra into a buffer until it hits a threshold that increments some variable that determines which frame of the portcullis tiles to display. The gate slams down if you step away before fully opening it - it might be a silent gif, but I reckon you can almost hear it thanks to the screen shake!

2 Likes

Playtime tracking is now implemented in Lolife!

lolife-playtime-tracking

I thought I'd share a quick rundown of how it works (ignoring the need to handle multiple save slots).

Essentially I'm keeping a running total of the number of frames that have passed in game, which I update in the game script's exit event:

on exit do
	stat_time_dframe = event.frame
	stat_time_dframe -= stat_time_last_frame
	stat_time_frames += stat_time_dframe
	stat_time_last_frame = event.frame
end

In practice there's a conditional that prevents that from counting time spent on the title screen, and of course stat_time_frames has to be restored when loading a save.

When I need to display playtime, I just call a playtime event like this to convert from frames to hours, minutes and seconds:

on playtime do
	t = stat_time_frames
	t /= 72000
	stat_time_hours = floor t
	t -= stat_time_hours
	t *= 60
	stat_time_mins = floor t
	t -= stat_time_mins
	t *= 60
	stat_time_secs = floor t
end

This method assumes the game has been running at a solid 20 fps, so in reality playtime is underestimated. It shouldn't be too inaccurate (thanks in part to all of my previous performance optimisation!) and in any case an underestimate is not as bad as an overestimate. I wouldn't want to falsely inflate the game's claimed runtime, that might feel dishonest!

Hopefully this'll be useful to anyone wanting to implement playtime tracking in their own pulp project :slight_smile:

4 Likes

It's been a little while, but progress on Lolife continues! The big news is that the game is now playable start to end without any major missing content. As this is a Pulp devlog, let's first talk about some of the more technical changes before looking at some new content.

Turn based mode improvements

Lolife lets the player toggle between "real time" and "turn based" gameplay. While it was working in principle, turn based mode previously felt a little slow and awkward, especially when out of combat. Alternating turns means after the player's turn there is a short delay for the entity turn, but this would happen even if there were no entities in the room or if any that were there were idle. Now the entity turn delay only happens if there are entities present who are moving or attacking the player. The result is much more intuitive, and it's the kind of thing players won't really notice (but they would have noticed had I left it how it was).

I've also doubled the speed of turns in turned based mode to make it effectively as fast as real time. This was just a small config change, but it makes a big difference to making it feel responsive and fun!

Loading multiple save slots

Lolife has multi-save support, something I got working early on to be easier to maintain. While this was working well, one shortcoming was that once loaded into a save game I would need to use fin (or have the player fully restart the game) to return to the main menu and load a different save. If I just used goto to return to the title screen and tried loading a different game, the game state wouldn't be fully reset because of tile swaps in rooms and some variables only being initialised on start.

While I haven't added the option for the player to quit back to the title screen (due to a lack of buttons and no access to the Playdate slide menu in Pulp), I did want to return the player to the title screen after beating the game, and I didn't want to use fin because of the limitations in how that would look (and the unnecessary delay of the entire game being restarted). To solve this I found all of the instances where I was swapping tiles based on progress flags and made them reversible.

For example a room script that was like this

on enter do
  if some_progress_flag==1 then
    tell x,y to
      swap "another tile"
    end
  end
end

now looks like this

on enter do
  if some_progress_flag==1 then
    tell x,y to
      swap "another tile"
    end
  else
    tell x,y to
      swap "original tile"
    end
  end
end

Now on beating the game the player is returned to the title screen instantly with goto and can continue, load a different save slot, or start a new game, and it should just work. I'm still considering if there is a nice way I can let the player quit back to the title screen on demand - maybe by docking the crank or double tapping B (and then asking for confirmation).

Dialogue menus

Lolife is an open world with "soft gates" between areas - barriers to progress that can be passed in a variety of ways, for example by collecting an optional item, fighting some enemies, or simply finding another more hidden path around. I don't really want to spoil many of the gates and the solutions (discovering those is a large part of the fun of the game) but I did want to show off this one room where the player wants to collect an optional key and can do so by either fighting the guards, sneaking around them, or giving the correct password (which varies - no spoilers here!). The guard dialogue combines a say and a menu to display the options as this gave me a lot more control than using ask.

lolife-key-fight
lolife-key-sneak
lolife-key-pass

Collectible orbs

While exploring the world of Lolife you might happen across these empty stone plinths. When inspected, each gives a clue to the location of a hidden orb. Smashing the corresponding orbs will return them to their pedestals. For what purpose, you will have to discover! Under the hood the orbs are implemented as entities like any others, just ones that do not move or attack. I added these optional, hidden collectibles as a means of filling out the world and making each area more dense with secrets to discover.

lolife-plinths

Cheat codes

There are cheats in Lolife! I won't reveal how many or how they might be discovered, but I did want to show off two in particular - enter the right codes on the title screen and you can play as Leit from Resonant Tale or The Choosed One from Initial Daydream!

There is a pcharacter variable that gets initialised as warrior (the default character) but is changed by the cheats. All of the player tiles are named like <pcharacter> <other stuff> so whenever I swap the player tile, I just start with {pcharacter} and it all just works!

lolife-cheat-characters

6 Likes

After some holiday polish Lolife is now content complete and ready for more playtesting by new players. Blind playtesting really is invaluable in highlighting problems I've overlooked because I'm too close to and familiar with the game!

In the meantime here is a quick "menu tour" showing off all of the options on the game's title screen. I'm generally very happy with the UX I've wrangled out of Pulp, especially with the look of the enemy codex and the in-game achievements ("awards").

lolife-menu-tour

One bit of polish I'm particularly proud of is how you can use the crank to rotate the enemies in the codex (shown here with a bull) and press A to play their attack animation.

Getting the attack animation to play nicely was quite fiddly in this case. In-game I simply use play which is short and simple, but here I am drawing the menu to the screen with window, label, etc. so using play isn't possible - the tile would be hidden beneath the drawn elements.

Instead I'm using draw to display the enemy sprite. The problem with that is you can't specify a frame when drawing a tile (and the tile has a non-zero FPS regardless) and unlike with play the tile won't animate from it's first frame - it is always looping based on an internal frame count inside Pulp.

One relatively simple solution would be to duplicate all of the enemy attacking sprites and use edited versions for the codex. I didn't like the idea of adding all those extra tiles though, so I tried to be smarter!

In order to synchronise the attack animation so the correct frames are displayed on pressing A, I made a single new tile with the appearance of a basic black tile but at 20 fps and with the number of frames enough to match the length of the enemy attacking tiles. This means its animation and frame count is in sync with the enemy attacking tiles.

That black attacking tile is placed in the corner of the title screen, and when the player presses A I call ignore and set a flag. That flag is checked every game frame and when set I use frame to check the frame count of the black attacking tile. On the game frame it equals 0 I know the attacking animation will be starting, so that's when I start drawing the attacking sprite to screen. After the known duration of the attacking animation I stop drawing it and call listen.

The end result is that the attack animation plays correctly on pressing A, albeit with a slight and variable delay from pressing the button to it actually playing. At worst it's still a short delay and in most instances the player won't notice!

8 Likes

This post is out of order chronologically with the above, but I also wanted to share that I put out a new game on itch.io just before Christmas called The Seven Bridges of Koenigsberg, a shortform narrative puzzle made in Pulp and based on the historical problem in mathematics.

seven_bridges_main_menu

There's not an awful lot to talk about technically with this one (it only uses a single room!) but I'm really happy with how it came out, especially visually with the auto-dithered art that I painted over to make the rivers pop. I'm proud of the font too, particularly the little hand icon pointing at the current selection in the menu! I know some people may struggle with legibility of a stylised half-width font, but I think it's necessary polish.

seven_bridges_about_the_puzzle

Another funny thing to note is that the majority of the final pdx size is taken up by the launch animation. That's larger than the game itself!

It's free or name your own price, so please do check it out!

7 Likes

I'm happy to report that Lolife playtesting has been giving me lots to work on! While I'm waiting on another round of playtesters I've also been adding some calendar events that trigger based on the Playdate's system clock and I thought it might be interesting to post about how I'm doing that for an Easter event specifically.

The challenge with Easter is that it's a moveable feast - the date changes every year. In Resonant Tale I had calendar events but I deliberately chose to base them on real-world celebrations that have a fixed date. As in other areas (like multi-save support) Lolife presents a good opportunity to improve on what I've done before.

There are two approaches we can take to handling the moving date of (Gregorian) Easter. Approach A is to simply look up the future date of Easter for every year for some amount of years into the future and hard code those into the game. With a simple (but long) chain of elseif datetime.year==20xx I could look up the date and be on my merry way (and not worry about anyone trying to play Lolife in a 100 years or whenever the cut-off is, which is a reasonable thing not to worry about). Approach B is to implement a known algorithm for determining the date of Easter given any year. It's more complex and probably overkill... but I went with Approach B, because that's more fun!

Before we can implement a known algorithm in Pulpscript, first we need to pick which algorithm. Being a popular subject, there are several. The most famous seems to be Gauss's Easter algorithm but (scrolling just past it) I instead opted for what Wikipedia calls the Anonymous Gregorian algorithm. It has the nice property of outputting the month n and the day of the month p which align with the format of datetime.month and datetime.day in Pulpscript.

The algorithm requires some basic arithmetic (addition, subtraction, multiplication, division) as well as the floor function and modulo operation. Pulpscript has a built-in floor function but no mod, so we have to implement that ourselves like so:

m = x
m /= y
_ = floor m
m -= _
m *= y
m = round m

which is equivalent to the expression m = x mod y

(A note on my personal pulpscript convention: I use _ as a temporary variable that is only relevant within a multi-line expression like this one. I also treat single-letter variables as "local" meaning I reuse them freely knowing I will never need to persist them between frames or across multiple events. It helps me keep my scripts neat!)

The round function call at the end is necessary due to rounding precision limitations. While the result of the line above m *= y should always be an integer, in practice it is a float with a value very close to but not quite an integer. If we don't round it off (to the correct value) it can lead to further errors when performing later modulo operations. (To demonstrate, 2 mod 2 should equal 0, but 2 mod 2.000000001 will equal 2.) This caught me out until I realised what was going on!

With that in place, here is an event that implements the Anonymous Gregorian algorithm in Pulpscript:

on getEasterDate do
	// Anonymous Gregorian algorithm
	// Provide Y as input year
	
	// a = Y mod 19
	a = Y
	a /= 19
	_ = floor a
	a -= _
	a *= 19
	a = round a
	
	// b = floor(Y/100)
	b = Y
	b /= 100
	b = floor b
	
	// c = Y mod 100
	c = Y
	c /= 100
	_ = floor c
	c -= _
	c *= 100
	c = round c
	
	// d = floor(b/4)
	d = b
	d /= 4
	d = floor d
	
	// e = b mod 4
	e = b
	e /= 4
	_ = floor e
	e -= _
	e *= 4
	e = round e
	
	// g = floor((8b + 13)/25)
	g = b
	g *= 8
	g += 13
	g /= 25
	g = floor g
	
	// h = (19a + b − d − g + 15) mod 30
	h = a
	h *= 19
	h += b
	h -= d
	h -= g
	h += 15
	h /= 30
	_ = floor h
	h -= _
	h *= 30
	h = round h
	
	// i = floor(c/4)
	i = c
	i /= 4
	i = floor i
	
	// k = c mod 4
	k = c
	k /= 4
	_ = floor k
	k -= _
	k *= 4
	k = round k
	
	// l = (32 + 2e + 2i − h − k) mod 7
	l = e
	l *= 2
	_ = i
	_ *= 2
	l += _
	l += 32
	l -= h
	l -= k
	l /= 7
	_ = floor l
	l -= _
	l *= 7
	l = round l
	
	// m = floor((a + 11h + 19l)/433)
	m = h
	m *= 11
	_ = l
	_ *= 19
	m += _
	m += a
	m /= 433
	m = floor m
	
	// n = floor((h + l - 7m + 90)/25)
	n = m
	n *= 7
	n *= -1
	n += h
	n += l
	n += 90
	n /= 25
	n = floor n
	
	// p = (h + l − 7m + 33n + 19) mod 32
	p = n
	p *= 33
	_ = m
	_ *= 7
	p -= _
	p += h
	p += l
	p += 19
	p /= 32
	_ = floor p
	p -= _
	p *= 32
	p = round p
end

And that's it! I'm actually then doing a little more maths because I want my calendar events to run for 5 days with 2 days either side of the date they are based around (and that can bridge a month boundary), but if you need to get the date of Easter in any year in Pulpscript the above event will do it for you!


Unrelated to the above, but to show something that isn't all code, here is the new card and launch animation I've made for Lolife (followed by starting a new game for the first time):

lolife-launch-to-new-game

10 Likes

I assume this means we'll see a release in time for us to enjoy this event this year. :thinking:

Well the great advantage of my approach is that I could release in 2026 or 2126 and it'd still work... :wink:

(But yes, hopefully releasing early this year! Catalog submission and release queue depending)

4 Likes

Great write-up! Thanks for the links too. Lunar new year next? :wink:

1 Like

For anyone following along with Lolife's development, a trailer just got shown during the 2024 Playdate Community awards! That was cut down to 30 seconds for the show, but you can watch the full length trailer here.

You can also now follow the game page on itch at Lolife by Orange Thief.

6 Likes

Given the wait for release I took a little break from Lolife before coming back to it for final bug testing and polishing and it has proven worthwhile! I've made several fixes and improvements that I might not otherwise have spotted or thought of without the time away, and I figure a few of them will (hopefully) make for an interesting post.

A "fun" and rare bug

In Lolife I have an entity system (which I've written about before). Every enemy is a type of entity, but other things are entities too where I want them to share some of the logic (like breaking things with the sword).

Guards are entities that can be spoken with. I implemented this with an interact event on the guard sprite. When you interact with a guard this event checks the targeted coordinates against the registered entities' coordinates to determine which entity is being spoken with, so an entity specific event can then be called allowing for different guards to have different dialogue despite sharing the same sprite.

When entering a room I register the entities it contains in the room script. There are up to 5 entities, and each has a set of variables associated with it. The main variables to consider are entity_n, entity_n_x, and entity_n_y. entity_n is the type of entity and entity_n_x and entity_n_y are the entity's coordinates.

Imagine a room that registers entity 1 and entity 2 where entity 2 is a guard. When the guard sprite is interacted with there is a chain of conditionals that first checks if the targeted coordinates are equal to those for entity 1, then for entity 2, etc. until they match. Naively I assumed, because 2 entities cannot occupy the same location in a room, this should work perfectly. Yet I found a bug where interacting with entity 2 was matching with the coordinates of entity 1 and therefore calling the wrong entity dialogue event (which in this particular case meant the guard's dialogue was missing).

How did this happen? It was the combination of a few things:

  • When changing rooms I was resetting the entity_n variables back to 0 but not resetting the entity_n_x and entity_n_y variables.
  • I was only conditionally registering entity 1 as it was not a regular enemy but a gate winch that can be permanently destroyed. Once destroyed I was only registring and therefore updating the variables for entity 2 (the guard).
  • In the previous room I was registering another enemy as entity 1 and it was possible that this enemy's coordinates could clash with the coordinates of the guard.

In other words the entity_1_x and entity_1_y variables were remaining as the coordinates of that entity's last position in the previous room and this could (with very bad luck!) match with the coordinates of entity 2 in the new room, which would confuse the dialogue fetching script into trying to fetch dialogue for the wrong entity in the room!

Once I figured this out the fix was easy - I now just make sure to also reset the entity coordinate variables when moving rooms.

A sound effect in-joke

A late bit of extra content I have added is an item called the "Blessed Ring". Its effect is to sometimes prevent the player from being killed by a deadly blow (leaving them with 1HP instead). When this happens I of course wanted to play a little sound effect...

But the sound effects were all completed a while ago before I even had the idea for this item and I didn't want to go back to Vic (who has made all the music and sounds) with a new demand and a short deadline!

Instead I decided to search through the existing sound effects for one I might reuse. I quite like doing this actually - it's a bit like how the clouds and bushes are actually the same sprite in Super Mario Bros, it can be fun and rewarding to be clever in asset reuse!

We made every entity type have its own set of sound effects so there are unique sounds for each enemy being attacked and being killed. These are named like mouse damage and mouse death. As I mentioned above entities can be things other than enemies, and one such entity type is called block. Block is literally a block that is found underground and you have to dig through by attacking it with your sword, Minecraft style. Thanks to the sound effect naming convention, destroying the block plays the sound block death. Maybe you already get the joke...

I was looking for a sound effect to reuse when the Blessed Ring activates and prevents the player from dying, and here was a sound effect called block death. Luck was clearly on my side as it sounds good in this new context too! So now you know the in-joke when that sound effect plays to tell you death was blocked :smile:

An obtuse hidden mechanic

Mice are the most common enemy in Lolife and their behaviour was pretty simple:

  • They moved about randomly
  • They became alerted if the player attacked any entity in the room
  • Once alerted they ran away from the player if the player moved within a certain radius

The new behaviour is this:

  • They move about randomly
  • They become alerted if the player attacks them directly
  • Once alerted they run away from the player if the player moves within a certain radius
  • If not yet alerted they can have one of two behaviours - either they ignore the player and move about randomly, or they are vigilant and will run away from the player if the player moves within a certain radius even without being alerted

What determines whether a mouse will be "vigilant" or not? It's randomly chosen for each mouse on entering a room with a chance equal to a hidden variable. That variable (the percentage chance of a mouse being vigilant) changes in the background every time a mouse is killed. If you kill a vigilant mouse the chance decreases by one percent. If you kill a non-vigilant mouse the chance increases by one percent.

In other words, it's player driven natural selection! Vigilant mice are actually easier to kill because their movement is more predictable, making them easier to chase down, but the more you kill the less likely they are to appear. It's self-balancing in that respect!

I don't really expect anyone to notice this when playing (at least not consciously), but I think it is fun regardless! And if you do know about it you can actually be selective in the mice you kill to push the balance one way or another (it min/maxes out at 1%/99% so you can't completely eradicate the two varieties, but you can get close).

5 Likes