What is Whale’s Waldo?

Reblogged from http://whaleswaldo.tumblr.com/post/171844761918

Whale’s Waldo is coming along smoothly, and we wanted to bring you an overview of the game and each of the guys you’ll meet. (All character art is in progress.)

You play as Ocean (renameable protag) who has recently returned home to New York City after an extended time away thanks to a stress-induced mental breakdown.

You and your childhood friend, Waldo, have always communicated with each other via lovingly handwritten letters where he details his adventures during his world travels. However, your hopes of learning about his latest exploits are dashed when you find that he hasn’t sent a single letter during the entire time you were away.

Resolved to learn what’s happened to your friend, you decide to travel to the location of his last letter – Bodo, in Norway – to see if you can find him. As your search reveals no leads, you continue to retrace his steps to the Philippines and then back to the United States, to see if he’s either returned there, or if any of the locals there happen to know what may have happened to him.

During your travels, you’ll meet and potentially befriend a wide variety of animals. With some luck, and if you play your cards right, you may even find someone – and somewhere – to settle down.

Waldo (the Sperm Whale)

Waldo is your childhood friend, and you grew up together in Rochester, New York. A little more than a year before the game starts, he left to travel the world, claiming that he just needed to see new sights, experience new things, and see if he found a place he thought he fit in. You’d always suspected there’s more to it than that, and after you return home to find that his usual letters have stopped coming, you set out to find him.

Stein (the Sea Urchin)

Stein is a cold, obnoxious urchin you meet on a ferry in Bodo, Norway. He has a secret passion that he may share with you, but he can also be a bit oblivious at times. If you manage to work your way through his hard exterior, he’ll warm up to you, and you might find that his punchy candidness is exactly what you’ve been missing in your life.

Octavio (the Blue-Ringed Octopus)

Octavio is a serious, uptight octopus who has grown distrustful of foreigners and visitors. He runs a hotel in Anilao in the Philippines. If you can show him that you’re responsible and considerate, you may find that he opens up to you.

Leonard (the Leaf Sheep Sea Slug)

Leonard is a small, cheerful slug who finds the positives in any situation, but wishes that his size didn’t make him so dependent on others. Despite being forgetful, he turns out to be an excellent guide to the city of San Francisco, California. If you manage to make enough of an impression on him, he may discover that you’re exactly what he’s been missing.

Jacob (the Great White Shark)

Jacob runs a bakery in Colorado Springs, Colorado, and would like nothing more than to fatten up bring joy to all humans with his delicious treats. If you manage to avoid his more forward advances and befriend him, you may discover that you’ve found someone you can share the rest of your life with.

February Updates

Sorry for the relative silence here. Figured I’d dump a general status post here before February is over.

March is nanoreno, and I’m signed up to work on the sea life dating sim Whale’s Waldo, which should be a lot of fun, but won’t leave me much time to work on other projects, like YAGS. 

Which is probably for the better anyway. YAGS itself is basically finished, minus assets (backgrounds, sprites, and music). If you’re been following the tumblr, you’ll notice that I’ve been writing a series of backstory fics while I’m waiting on assets. My goal is to post one fic a week for as long as I can… either until I run out of things to write about, or I start ZAGS development in earnest. I’m hoping nanoreno won’t interfere with this goal too much.

And speaking of assets: There’s not much progress to report there. Jake is complete, and will be added to the game at some point, but my musician has run into personal issues, so staging music will be further delayed a bit. Sprites and backgrounds otherwise are ongoing.

Fic: At First Sight

A random comic showed up in my feed that has the same general idea as this piece, so this seemed like as good a time as any to post it.

This is a random fic I threw together a month ago when I couldn’t sleep. It’s going to be the basis for a short visual novel project that I want to make at some point (see my attempt at sprites for it in the last post).

My original plan was to do this for nanoreno in March, but that seems unlikely now given my other commitments. We’ll see how that works out.

Obligatory warning for strong language and discussions of sex.

(For those of you following my series of YAGS fics, this is completely unrelated to those and the YAGS universe. So don’t worry about spoilers.)


At First Sight

Everyone has a soulmate.

It seems ridiculous to write that, even now. I mean, of course they do. It’s a fact as obvious as the sun rising every morning or the planet continuing to orbit the sun. Even babies, wide-eyed and babbling, stuck in their endless cycle of eating and shitting, somehow have an innate understanding of that fact as their mothers – and fathers; okay, it is the 21st century after all – drag them around on shopping trips to be poked and prodded and cooed over by strangers in the aisle of the grocery store. Even they, somehow, know this, and the lucky ones – the really, really lucky ones – are wheeled past one another between the rows of canned corn and dry pasta and then never leave each others’ sides again.

Everyone has a soulmate, and you’ll know the first time you lay eyes on them.

Okay, the second part is maybe a little less obvious. It’s strange then, maybe, that no one really talks about it. But it explained why I was sitting here, in a shitty run-down bar drinking watery tequila with one eye on the door and the other on the clock.

The dreams had started a few days ago. I was old – pushing 30 years, at that point – trying to deal with the fact that I was one of those unlucky few who would go their lives without that glimpse into the future that would help find that elusive them. Or her, anyway.

30 years old, trying to deal with the fact that fewer and fewer of my friends – really acquaintances because, let’s be real, no one really wants to be friends with a thirty-something-year-old guy still off on his own – sent wedding invitations each year, that stupid “plus one” staring at me from the crisp white cards, as if mocking me. That more and more of them were having kids of their own, eating and shitting and babbling their endless streams of nonsense.

What a load of crap.

So it was somewhat of a relief when I had the first dream, vague as it was. I wasn’t even sure it was a premonition at that point because, you know, no one really talks about it. Which seems silly because, hey, it happens to everyone, right? Thank god for the internet, at least.

But then I had it again, a little clearer the next night. And the next. And so on, until today when my bus home popped a flat, and I decided to walk, and in my infinite wisdom managed to screw up the route so badly that I ended up in front of this bar that I’d never seen before, staring at the dusty wooden letters above the rickey old door, and feeling a lump in my throat because I knew this place. I knew this place, and I knew that this was it.

I took another shot, noticing the bartender watching me – concern maybe? disgust? – and glancing over at the door again. I also knew that she was late. But, somehow, with that expectation, maybe she’d be here exactly when she was supposed to be?

The door shuddered open again, and my eyes darted back to it – when did I look away? – and she walked through.

More accurately, they walked through.

I started into their eyes, their arms around each other, their smiles fading as I watched and we watched and we knew. All three of us, somehow, knew.

“What the… fuck?”


We sat at a booth – although calling it that would be generous – awkwardly trying to look everywhere but each other, the butterflies in my stomach having given way to some deep, gnawing pain, instead, and my head spinning a bit more than I’d like, not just due to the knowing.

This was all wrong.

One of them, the blonde one, clears his throat and sticks out his hand.

“Hi. I’m, uh… I’m Tim.” He jerks his head toward the other guy, who’s trying his hardest to pick the cracking ends of the cheap linoleum tabletop to pieces. “That’s Matt. We, uh…”

The sound of a couple of glasses hitting the table sounds like a gunshot, and the bartender nods at us, quickly, before returning to his post. Tim’s hand hovers, awkwardly, and I grab it because, hey, it’s only polite, right? If only the damn walls would stop moving.

“What’s… what’s your…”

“Chris.”

He nods, and I make the mistake of looking at him again, those blue eyes drawing me in, in the dream feeling like an oasis in the desert but now, somehow, threatening to drown me. I tear my gaze away from him and focus on the puddle of tequila in my shot glass.

“I’m not gay.”

The other guy, Matt was it?, laughs at that. It’s not even a particularly nice laugh, soft and pleasant – no, it’s rough, like nails on a chalkboard in the foggyness of my brain, and I know I should stand up and walk out of here, now.

“Funny, coming from a guy drunk on shots of tequila in a gay bar.”

“Matt. That’s not…”

“Hey, just pointing out the obvious.”

Tim bites his lip, fingers awkwardly drumming the table.

“We, uh. We should talk about this.”

“You’re supposed to be a woman.” The words come out, angrier than I intend, perhaps, and slurred from the booze, and Matt just stares at me, this stupid grin on his face as Tim takes a huge gulp of his drink. Then he’s laughing again, and I’m shaking my head. “No. Stop that, dammit. You’re supposed to be a woman. Emphasis on the ‘a’. As in one. Person. Also female.”

“That’s why we should talk about this.” He takes another gulp, the obnoxiously pink liquid sliding down his throat, and my eyes lingering, somehow, there and on the way his adam’s apple bobs as he does. “I, uh… we, I guess, uh… we all felt that, right? That knowing.”

“Babe, you’re stating the obvious again.”

He puts his hand on Tim’s arm, and I stare at it. In revulsion? Disgust? Why the hell was I feeling this way? I don’t want some faggot’s hand on my arm.

Must be the liquor talking.

“Well, it’s not exactly something people talk about. And, uh… not exactly something I expected to ever feel again, in my lifetime.”

“What, we don’t stare into each others’ eyes every night while you’re buried deep inside me?”

Tim turns red and stares at the empty glass in front of him, a drop of pink slowly running down the side.

“Is that supposed to make me feel better?” My stomach is outright revolting now, threatening to unleash its wrath upon the unsuspecting tile floor.

“Chris. Dude. I don’t know what this is either, okay? But we all know it’s right. So why don’t we…”

My stomach makes itself known, then, dumping its contents, the sickly stench of tequila and whatever crappy lunch I’d had that day permeating the air, and Matt is laughing again, and I want to tell him to shut up and walk away. I want to punch that stupid face and walk out of here and never see either of them again.

Instead, I take sips of the water he brings me, and let sleep take over.


I wake up in an unfamiliar bed, in an unfamiliar room, the pounding in my head and cotton in my mouth adding to the confusion. There’s a trashcan by the bed, and I grab it, dry heaving as my stomach tries, unsuccessfully, to rid itself of the emptiness inside it.

Tim walks in, a glass of water and some pills in his hand, setting them on the table and sitting on the bed, next to me.

“Hi.”

I look at him, not entirely friendly, and he stares at the floor.

“What did…”

“You passed out. We weren’t sure where you lived or anything, so we brought you back here, instead.” He hands the glass and pills to me, still not meeting my gaze. “Nothing happened, if that’s what you’re asking. We just…” He bites his lip again. “We just wanted to make sure you were okay.”

I eye the pills warily, deciding they’re just painkillers, before downing them and half the glass of water. He watches me, and our eyes lock, neither of us able to look away until Matt erupts into the room, sees us, and starts laughing.

“Having a moment?”

“Matt, that’s not…”

“Hey, I want in on this too.”

He sits down next to Tim, an arm over his shoulder, smiling at me with that stupid grin on his face.

“Feeling better?”

I shake my head, the light too bright and my head still pounding, and he laughs again.

“Dude. Ten shots? I’m not surprised. Also you should make sure you go pay the bartender tomorrow… we convinced him to start a tab for you, but he doesn’t exactly have a card for you, so we’re gonna be i…”

“Matt. We should let him sleep.”

“Where are we going to sleep?”

“Our couch pulls out, remember?”

“But babe. We…”

I shake my head, shimmying out from under the covers and feeling the unfamiliar carpet under my feet.

“That’s okay. I should, uh… get out of your hair, anyway.”

Tim bites his lip again and Matt shakes his head.

“Uh uh. You’re staying somewhere we can keep an eye on you, dude. Just maybe not exactly there.” I open my mouth to protest, and end up in the plastic haven of the trash can again. Matt just laughs, does an I-told-you-so, and kicks me out of the bed.


I end up on the couch, staring at the ceiling in the darkness, my stomach still doing flips but not angry enough to fight, anymore. There’s snoring coming from the other room, and I wonder if it’s Tim or Matt.

It’s oddly endearing.

No. It’s shit, and it’s annoying, and what the hell am I still doing here, lying on some stranger’s – no, strangers’ – couch with vomit on my shirt and the worst hangover I think I’ve had, ever?

No one has ever rejected their soulmate. At least, not that I’d ever heard. Then again, I’d never heard of anyone having two, either.

“I’m not gay.”

It comes out, softer and maybe a bit more hoarse than I’d like. But It’s comforting, somehow, to say it out loud to the darkness. And that slight comfort, or whatever, was enough to let me drift off to sleep.


So this is probably the part where you expect a sort of coming out montage, where I find my inner Cher, or RuPaul, or whoever, and start taking it in the ass from Tim and Matt, right?

Well, sorry to disappoint, because sexuality doesn’t work like that, no matter how much I may have wanted it to, or not wanted it to.

That’s not to say they didn’t try – I had a rather interesting, and honestly rather pleasurable, experience on the receiving end of some blowjobs. But straight is straight, and gay is gay, and there’s just something about another man’s lips wrapped around your cock that is an instant boner killer, you know?

So no, we didn’t get it on every night with hot threesomes that kept the neighbors up.

What we found, instead, was somehow even better than that – a sort of closeness and companionship that you can’t really describe with words. It was perhaps the first time I watched them having sex, seeing the way they looked at each other, and somehow getting caught up in the moment myself, despite not caring for the particulars; it was perhaps that time, and the way they also looked at me in their throes of passion or whatever, that made me feel like, somehow, this was where I belonged.

I’ll spare you all the gory details of our arrangement – suffice to say, arranging hookups with un-soulmated women wasn’t particularly difficult – but it somehow worked for us. A week after that fateful day in the bar, I broke my lease, packed up my shit, and moved in with them to their cramped little apartment in the middle of nowhere, and none of us have looked back since.

Well, no. I suppose there’s one thing I need to fix.

Everyone has a soulmate, and you’ll know the first time you lay eyes on them. But maybe not just one.

Upcoming Projects

randomnobodyandfriends asked:Really enjoyed YAGS, will definitely play it a few more times once finished. If you don’t mind sharing, do you have any idea what genre your next project will be?

Thank you for the message!

I expect YAGS will be code-complete by the end of February, with both final backgrounds and music, leaving only sprites to go. Those should filter in over the next few months, so hoping for a final release sometime this summer. (But with plenty of releases between now and then with sprite updates.)

I have two projects I want to work on while I’m waiting for sprites:

The first is ZAGS, a direct sequel to YAGS, but built more as a stat-based dating sim, because I really want to get into more advanced RenPy coding, and I think setting up weekly scheduling with interspersed VN scenes would make for an interesting project. YMMV in terms of play, because I expect it will feel extremely different than YAGS.
This also has the benefit of reusing many assets from YAGS, so its release won’t get delayed waiting for sprites. And hopefully I’ll be able to plan in several CGs as well.
(Screenshot below of the current GUI, as implemented in RenPy, including the stats summary screen.)

The second is intended to be a short (5-10k word) VN based off of a drabble I wrote one night while I couldn’t sleep. It’ll probably be in RenJS, and I mostly want to pursue this project as a way to handle all art assets for a game myself. (Some sketches of the sprites are below. They look pretty weird. I’m clearly not an artist.) Story-wise, it’s a slight fantasy twist on reality?

Of course, given my lack of motivation on things sometimes, it’s also possible that neither of these things will actually come to pass.

(It’s also worth noting that the second of these projects was my primary reason for splitting my tumblr up yesterday, so expect more details about it in the relatively near future.)

Codes: Character Profiles

Another thing the game has (and will build on in the next release) is unlockable character profiles with some information about each character, in addition to some bonus features unlocked from them. As far as things I’ve posted here go, this is a fairly straightforward one, but I figured I’d share anyway.

First of all, I defined a block of data for each character’s information. In addition, because these are linked from the title screen via a sketched sprite, I included coordinates for the appropriate sprite.

python early:
# list in reverse order of rendering on title screen
real_profile_names = (
"nick", "hannah", "elliot", "nate", "lizb", "lizg", "adam", "juan", "jake", "dan", "james",
)
profile_profile_info = {
"adam": ("Adam", (950,340),
"Sophomore", "Biology", "August 15, 1986 (age 20)", "6'2\"", "California, United States",
"Adam James Prewitt",
"Profile text for Adam"),
"juan": ("Juan", (850,340),
"Junior", "Biology", "April 22, 1986 (age 20)", "6'0\"", "Costa Rica",
"Juan Rodrigo Salas Valverde",
"Profile text for Juan"),
…etc…
}

Next we actually put these on the title screen.

screen main_menu():
…Stuff…
fixed:
if "completed" in persistent.profiles:
imagebutton auto "images/title/leigh_%s.png" action Show("char_profile", chname="ashleigh") xanchor 0.0 yanchor 0.0 pos (650, 140)
imagebutton auto "images/title/leigh_%s.png" action Show("char_profile", chname="leigh") xanchor 0.0 yanchor 0.0 pos (850, 140)
imagebutton auto "images/title/steve_%s.png" action Show("char_profile", chname="steve") xanchor 0.0 yanchor 0.0 pos (950, 140)
imagebutton auto "images/title/robert_%s.png" action Show("char_profile", chname="robert") xanchor 0.0 yanchor 0.0 pos (1050, 140)
for profname in real_profile_names:
if profname in persistent.profiles:
imagebutton auto "images/title/" + profname + "_%s.png" action Show("char_profile", chname=profname) xanchor 0.0 yanchor 0.0 pos profile_profile_info[profname][1]

Next, we define the screen for character profiles. Note the additional features that I’ll get into shortly.

screen char_profile(chname):
tag charprofile
xpos 0
ypos 0
default pmode = "normal"
fixed:
add "images/sp/profilebg.png" xpos 0 ypos 0
if pmode == "best":
add "images/title/profile_" + chname + "best.png" xalign 0.5 yalign 1.0 xpos 175 ypos 720
elif pmode == "good":
add "images/title/profile_" + chname + "good.png" xalign 0.5 yalign 1.0 xpos 175 ypos 720
else:
add "images/title/profile_" + chname + ".png" xalign 0.5 yalign 1.0 xpos 175 ypos 720
if (chname == "james" and persistent.permacc_james) or (…other guys’ checks…):
add "images/title/profile_" + chname + "acc.png" xalign 0.5 yalign 1.0 xpos 175 ypos 720
if chname + "best" in persistent.profiles or chname + "good" in persistent.profiles:
imagebutton auto "images/sp/proftab_normal_%s.png" action SetScreenVariable("pmode", "normal") xanchor 0.0 yanchor 0.0 pos (375, 631)
imagebutton auto "images/sp/proftab_good_%s.png" action SetScreenVariable("pmode", "good") xanchor 0.0 yanchor 0.0 pos (375, 559)
if chname + "best" in persistent.profiles:
imagebutton auto "images/sp/proftab_best_%s.png" action SetScreenVariable("pmode", "best") xanchor 0.0 yanchor 0.0 pos (375, 480)
imagebutton auto "images/sp/profextra_%s.png" action Start("epilogue_bonus_" + chname + "1") xanchor 0.0 yanchor 0.0 pos (350, 0)
imagebutton auto "images/sp/profpermacc_%s.png" action ToggleField(persistent, "permacc_" + chname) xanchor 0.0 yanchor 0.0 pos (363, 171)
vbox xpos 500 ypos 30 xalign 0.0 xsize 480:
spacing 10
text "{size=40}" + profile_profile_info[chname][0] + "{/size}"
text "{size=25}Name: " + profile_profile_info[chname][7] + "\nYear: " + profile_profile_info[chname][2] + "\nMajor: " + profile_profile_info[chname][3] + "\nBirthday: " + profile_profile_info[chname][4] + "\nHeight: " + profile_profile_info[chname][5] + "\nHome: " + profile_profile_info[chname][6] + "{/size}"
text "{size=25}" + profile_profile_info[chname][8] + "{/size}"
vbox xpos 1250 ypos 30 xalign 1.0 xsize 400:
for k in sorted(profile_profile_info):
if k in persistent.profiles or ("completed" in persistent.profiles and k in ("ashleigh", "leigh", "steve", "robert")):
textbutton profile_profile_info[k][0] action [Show("char_profile", chname=k), SelectedIf(chname == k)] xalign 1.0
textbutton "Return to Main Menu" action Hide("char_profile") xalign 0.0 xpos 500 ypos 675

As you can see, the profile screen has a few unlockable features. The first feature is buttons that let you view alternate poses for the character image, depending on which ending you’ve completed with the given person. This is implemented simply, with a screen variable indicating the mode.

The second feature is the ability to access a bonus scene for the given character, once you’ve found their HEA ending. Just define a label with the appropriate name (epilogue_bonus_james1 for example).

The last feature is a toggleable item on the guy that will persist on their sprite during subsequent playthroughs. For example, if you get James’ best ending, you can toggle his enagement ring on or off, and have it show up whenever he appears in the game. The associated code for that builds off of our sprite code and simply adds a new check for the persistent variable, and a layer.

image james jamesnew = LiveComposite(
(266, 700),
(0, 0), "chnew/james/[spposes_data[james].pose]/base.png",
(0, 0), "chnew/james/[spposes_data[james].pose]/basehair.png",
(0, 0), "chnew/james/[spposes_data[james].expression].png",
(0, 0), "chnew/james/[spposes_data[james].pants].png",
(0, 0), "chnew/james/[spposes_data[james].shirt].png",
(0, 0), "chnew/james/[spposes_data[james].permacc].png",
(0, 0), "chnew/james/[spposes_data[james].extras].png",
)
    def execute_showsp(o):
…old code…
# Handle epilogue accessories
if who == "james" and persistent.permacc_james:
store.spposes_data[who].permacc = help_get_filepathsp(who, pose, "x", "ring")
…other guys' accessories…
else:
store.spposes_data[who].permacc = help_get_filepathsp(who, pose, "x", None)
…old code…

And… that’s it. Obviously, your profiles don’t have to be nearly this complicated. If you just want to show some basic information for each character, the screen and associated logic will be much simpler.

Codes: Sprite expression changes

Last time, we added a custom showsp command to easily show sprites that obey game settings automatically. But that can be tedious to use if all you want to do in a scene is change a character’s expression when they’re already onscreen. Cue showexp!

A couple of changes to the old code are necessary. First of all, we need to store the raw names for the clothing (you’ll see why in a moment).

python early:
class CharPoses:
pose = ""
pants = ""
shirt = ""
expression = ""
extras = ""
oldclothes = ""
basepantsstr = ""
baseshirtstr = ""
baseextrasstr = ""
def execute_showsp(o):
…same as before…
if not who in store.spposes_data:
store.spposes_data[who] = CharPoses()
store.spposes_data[who].pose = pose
store.spposes_data[who].baseshirtstr = shirt
store.spposes_data[who].basepantsstr = pants
store.spposes_data[who].baseextrasstr = extras
store.spposes_data[who].shirt = help_get_filepathsp(who, pose, "s", shirt)
store.spposes_data[who].pants = help_get_filepathsp(who, pose, "p", pants)
store.spposes_data[who].expression = help_get_filepathsp(who, pose, "e", expr)
store.spposes_data[who].extras = help_get_filepathsp(who, pose, "x", extras)
…same as before…

Next, we define the parser and lint for showexp. Note that we’re allowing changes to the pose, as well, so this can be called as either showexp adam happy or showexp adam happy crossed.

python early:
def parse_showexp(lex):
who = lex.simple_expression()
expr = lex.simple_expression()
if lex.eol():
return (who, None, expr)
pose = lex.simple_expression()
return (who, pose, expr)
def lint_showexp(o):
who, pose, expr = o
if len(o) != 3 or expr is None:
renpy.error("Invalid showsp declaration")
return
if not who in showsp_valid_combos:
renpy.error("Invalid person " + who)
return
if pose is None:
# don't know the pose so check across poses
# may fail if a given expr only exists for one pose
# but we can't track current pose state
hasExpr = "e_" + expr in showsp_valid_combos[who]["!any"]
if not hasExpr:
for pose in showsp_valid_combos[who]:
hasExpr = "e_" + expr in showsp_valid_combos[who][pose]
if hasExpr:
break
if not hasExpr:
renpy.error("Invalid expression " + expr + " for " + who)
else:
if not pose in showsp_valid_combos[who]:
renpy.error("Invalid pose " + pose + " for " + who)
return
if not "e_" + expr in showsp_valid_combos[who][pose] and not "e_" + expr in showsp_valid_combos[who]["!any"]:
renpy.error("Invalid expression " + expr + " for " + who + pose)

Note the documented limitation in linting. Since we don’t always have the pose information, if you have an expression that exists only for one pose, this will be missed by linting. (So make sure all expressions exist in some base form, and override them on a per-pose basis as necessary, to prevent this.)

Finally, the actual execute command, where we use the information we’d previously stored in the new fields:

    def execute_showexp(o):
who, pose, expr = o
if not who in store.spposes_data:
renpy.error("Invalid person " + who)
if pose is not None:
if store.spposes_data[who].pose != pose:
# force all other components to re-render as necessary when pose changes
store.spposes_data[who].shirt = help_get_filepathsp(who, pose, "s", store.spposes_data[who].baseshirtstr)
store.spposes_data[who].pants = help_get_filepathsp(who, pose, "p", store.spposes_data[who].basepantsstr)
store.spposes_data[who].extras = help_get_filepathsp(who, pose, "x", store.spposes_data[who].baseextrasstr)
store.spposes_data[who].pose = pose
else:
pose = store.spposes_data[who].pose
store.spposes_data[who].expression = help_get_filepathsp(who, pose, "e", expr)
renpy.register_statement("showexp", parse=parse_showexp, execute=execute_showexp, lint=lint_showexp)

This will probably also fail if a particular shirt, pants, or extra exists only for one pose and you change pose as part of the command. Again, to be safe, make sure everything a character could be wearing exists at some form at the base level, or just don’t change poses around when using the command.

In any case, now you can more easily carry on a conversation without having to remember what a character was wearing in the same scene (or even where they’re standing, which is an improvement over the basic game’s show command, as well).

showsp adam sides greent jeans happy
adam "This is only a test. How do I look?"
showexp adam worried
adam "Does this look okay?"
showsp adam crossed bluet slacks curious jacket left
adam "Now I'm over here, and in blue. Huh."
showexp adam confused
adam "Strange."
showexp adam default crossed
adam "Oh well."
showexp adam grin sides
adam "It's nice to meet you anyway."

Yay! One last thing: Calling showexp before showsp will probably do nothing, since changing variables around while a sprite is offscreen has no effect. So if your script has a lot of jumps in it, you probably want to lead each label with a showsp for all of your characters, again, just to be safe.

Codes: Sprites, the YAGS way

One thing I needed was the ability to easily show sprites that obeyed various game settings automatically. For example, there’s a toggle in preferences to revert to the legacy stick figure sprites, there’s a nudity toggle, and there may eventually be an option to toggle body hair. In addition, the guys have a lot of different possible combinations of clothing and expressions.

There are a lot of “automatic” sprite scripts out there already, this oneprobably being one of the best ones. And for most people, I’m sure that will work just fine.

My requirements would have made that script tedious to use, though, since I would have had to case on different flags every time I wanted to show a sprite. Instead, I threw together a custom show command (showsp) that gives me the flexibility to change sprites depending on persistent and game state.

The first thing to do is to define the actual sprites. Since I’m supporting both the new style and old stick figures, each character has to be defined twice. Note the file structure requirements in the comment. It’s also worth noting that each expression must exist both in the old and new sprite directories. 

default spposes_data = {}

#file structure: base folder adam
#under that, folders with name of poses (sides, crossed) with base.png under it
#then either in base folder or in poses subfolder
#s_.png, p_.png, e_.png, x_.png
#in poses folder if differs by pose, base folder if not
#x_blank.png and p_cover.png is required for all characters
#s_shirtless, p_underwear, and p_naked are handled specially for legacy sprites, if they exist
image adam adamnew = LiveComposite(
(342, 600),
(0, 0), "chnew/adam/[spposes_data[adam].pose]/base.png",
(0, 0), "chnew/adam/[spposes_data[adam].pants].png",
(0, 0), "chnew/adam/[spposes_data[adam].shirt].png",
(0, 0), "chnew/adam/[spposes_data[adam].expression].png",
(0, 0), "chnew/adam/[spposes_data[adam].extras].png",
)
#c_blank.png and c_basic.png are required for clothing in old sprites
image adam adamold = LiveComposite(
(500, 600),
(0, 0), "chold/adam/base.png",
(0, 0), "chold/adam/c_[spposes_data[adam].oldclothes].png",
(0, 0), "chold/adam/[spposes_data[adam].expression].png",
)

Also note we’re making use of dynamic images, which is particularly useful, and would work on its own, except you don’t get linting checks for invalid images, and it would make assignment within the game a bit of a pain.

Next, we actually define a command to show sprites. This is the heart of the framework, so I’ll go over it piece by piece.

First is the file processing. This automatically picks up all images from the chnew directory, sorts them by pose, and builds a map of all allowable assets. 

python early:
class CharPoses:
pose = ""
pants = ""
shirt = ""
expression = ""
extras = ""
oldclothes = ""
showsp_valid_combos = {}
showsp_valid_locations = ("left", "right", "slightleft", "moreright", "lessright")
def process_file(charname, pose, filename):
itemname = filename.replace(".png", "")
if itemname != "base":
if not pose in showsp_valid_combos[charname]:
showsp_valid_combos[charname][pose] = set()
showsp_valid_combos[charname][pose].add(itemname)
def process_all():
for path in renpy.list_files():
if path.startswith("images/chnew/"):
pathlist = path.split("/")
charname = pathlist[2]
if not charname in showsp_valid_combos:
showsp_valid_combos[charname] = {}
showsp_valid_combos[charname]["!any"] = set()
if pathlist[3].endswith(".png"):
process_file(charname, "!any", pathlist[3])
else:
process_file(charname, pathlist[3], pathlist[4])
process_all()

Note the un-ideal manual declaration of supported Positions. This will show up again later, but I’m not sure how to take a position string and convert it to a Position object automatically.

Next, the custom command itself. The parsing logic is slightly complex, because we want to be able to omit the position and omit extras, in any combination. (Note that this technically means you can’t have both a Position and an extra with the same name, but you probably don’t want something like x_right.png anyway.)

python early:
def parse_showsp(lex):
who = lex.simple_expression()
pose = lex.simple_expression()
shirt = lex.simple_expression()
pants = lex.simple_expression()
expr = lex.simple_expression()
if lex.eol():
return (who, pose, None, shirt, pants, expr, None)
extraorpos = lex.simple_expression()
if lex.eol():
if extraorpos in showsp_valid_locations:
return (who, pose, extraorpos, shirt, pants, expr, None)
else:
return (who, pose, None, shirt, pants, expr, extraorpos)
else:
return (who, pose, lex.simple_expression(), shirt, pants, expr, extraorpos)

Next, the linting. It’s technically unnecessary, but it’s invaluable unless you want to be tracking down runtime errors from invalid clothing or expressions in your game. Note the returns that prevent errors when you try to access a map item that doesn’t exist (due to invalid pose or character).

    def lint_showsp(o):
who, pose, where, shirt, pants, expr, extras = o
if where is not None and where not in showsp_valid_locations:
renpy.error("Invalid location " + where)
if not who in showsp_valid_combos:
renpy.error("Invalid person " + who)
return
if not pose in showsp_valid_combos[who]:
renpy.error("Invalid pose " + pose + " for " + who)
return
if not "s_" + shirt in showsp_valid_combos[who][pose] and not "s_" + shirt in showsp_valid_combos[who]["!any"]:
renpy.error("Invalid shirt " + shirt + " for " + who + pose)
if not "p_" + pants in showsp_valid_combos[who][pose] and not "p_" + pants in showsp_valid_combos[who]["!any"]:
renpy.error("Invalid pants " + pants + " for " + who + pose)
if not "e_" + expr in showsp_valid_combos[who][pose] and not "e_" + expr in showsp_valid_combos[who]["!any"]:
renpy.error("Invalid expression " + expr + " for " + who + pose)
if extras is not None and not "x_" + extras in showsp_valid_combos[who][pose] and not "x_" + extras in showsp_valid_combos[who]["!any"]:
renpy.error("Invalid extra " + extras + " for " + who + pose)

Finally, the actual command logic:

    def help_get_filepathsp(who, pose, prefix, item):
if prefix == "x" and item is None:
return "x_blank"
if prefix + "" + item in showsp_valid_combos[who][pose]: return pose + "/" + prefix + "" + item
elif prefix + "" + item in showsp_valid_combos[who]["!any"]: return prefix + "" + item
else:
renpy.error("Could not find resource " + prefix + "_" + item + " for " + who + pose)
def execute_showsp(o):
who, pose, where, shirt, pants, expr, extras = o
whereobj = None
if where is not None:
if where == "left":
whereobj = left
elif where == "right":
whereobj = right
elif where == "slightleft":
whereobj = slightleft
elif where == "moreright":
whereobj = moreright
elif where == "lessright":
whereobj = lessright
else:
renpy.error("Unknown location " + where)
else:
whereobj = default
# TODO: Special variable checks to override things here
# possibly on a character-specific basis
if pants == "naked" and not persistent.nudes:
pants = "cover"
# End special checks
if not who in store.spposes_data:
store.spposes_data[who] = CharPoses()
store.spposes_data[who].pose = pose
store.spposes_data[who].shirt = help_get_filepathsp(who, pose, "s", shirt)
store.spposes_data[who].pants = help_get_filepathsp(who, pose, "p", pants)
store.spposes_data[who].expression = help_get_filepathsp(who, pose, "e", expr)
store.spposes_data[who].extras = help_get_filepathsp(who, pose, "x", extras)
# handle old sprites; either they're dressed or they're not
if pants == "naked" or pants == "underwear" or pants == "cover" or shirt == "shirtless":
store.spposes_data[who].oldclothes = "blank"
else:
store.spposes_data[who].oldclothes = "basic"
if persistent.oldsprites:
renpy.show(who + " " + who + "old", at_list=[whereobj])
else:
renpy.show(who + " " + who + "new", at_list=[whereobj])
renpy.register_statement("showsp", parse=parse_showsp, execute=execute_showsp, lint=lint_showsp)

A few things to note here: The position stuff is really non-ideal (you need to manually handle every position you support for sprites in the game), but you can’t just do at_list=[where] as where is a string and not a position. There’s special logic to handle overrides, such as putting a covering over the character if they’re naked and nudity is turned off; it would be straightforward to handle body hair the same way.  You can also override special cases manually (for example, I’ll probably handle Juan’s cast this way by reading the current game state instead of having to put the cast as an extra every time).

This should also be easily extensible if you want to build expressions from pieces (eyebrows, eyes, mouth) by adding support for things like b_, y_, and m_.

In any case, all of this mess of code allows you to write your game script like this:

showsp adam sides greent jeans happy jacket left
adam "This is only a test. How do I look?"
showsp adam crossed bluet slacks curious right
adam "Now I'm over here"
showsp adam crossed shirtless jeans embarassed
adam "And losing my clothing."
showsp adam sides shirtless underwear embarassed jacket
adam "That's not really any better."

Which is really nice given it’s obeying all of your game state and preferences automatically.

Future enhancement: A showexp command that changes just the expression on the given sprite, without having to re-specify the rest of the clothing or position.

Edit: The showexp command has been defined here. Also, I found a way to grab the location object based on the string name for it.

        if where is not None:
if where in showsp_valid_locations:
whereobj = globals()[where]
else:
renpy.error("Unknown location " + where)
else:
whereobj = default

I kept the showsp_valid_locations list in place, because it seemed useful. (After all, you want to make sure what you’re grabbing is actually a Position object that you expect, and not some arbitrary RenPy object sitting there.) But this means adding a new position is just a matter of defining it and updating the list, instead of actually updating code.

Codes: Achievements, Take Two

Last time, we added achievements to our game, in a rather crappy and user un-friendly way, but that should theoretically integrate with Steam properly. Today, we improve that by defining a custom achievement command and showing a message to the user!

The first thing to do is to define a command:

python early:
basic_achievements = (
("First Steps", "Starting your college life", "Starting your college life"),
…More achievements…
)
def parse_achieve(lex):
what = lex.rest()
return what
def lint_achieve(what):
if not any(what == a for a,lkd,unk in basic_achievements):
renpy.error("Invalid achievement " + what)
def execute_achieve(what):
if achievement.has(what):
return
achievement.grant(what)
renpy.show_screen("gain_achieve", _layer="screens", achievetext=what)
renpy.register_statement("achieve", parse=parse_achieve, execute=execute_achieve, lint=lint_achieve)

Note the screen that we show to notify the user. That screen is defined as follows:

screen gain_achieve(achievetext):
tag achievementalert
fixed:
image "sp/achievementbg.png" at Position(xpos=0.85, ypos=0.1, xanchor=0.5, yanchor=0.5)
text "{size=22}You unlocked an achievement:\n" + achievetext + "{/size}" id "achievetext" at Position(xpos=0.85, ypos=0.1, xanchor=0.5, yanchor=0.5) xmaximum 290 ymaximum 90
timer 3.0 action Hide("gain_achieve")

Basically, we’ve added a new screen that that shows up in the upper right of the screen, tells the user they unlocked an achievement, and then dismisses itself after 3 seconds.

One important thing I want to point out here is the _layer=“screens”parameter when you show the screen with renpy.show_screen(). Assuming I understand RenPy properly, by default, there are a few different threads to render the GUI. In particular, when you show a screen, the main rendering thread is blocked on the screen, and will not do anything else until you dismiss it. In this case, because we’re showing a screen that dismisses itself, RenPy freaks out and throws an error on the next call that does anything other than hides the screen.

When we direct the screen to be displayed in the overlay layer, instead, it does not tie up the main thread. Indeed, gameplay continues normally and the user can keep playing.

The final part of this is to actually grant achievements within your game script. Because we’ve defined a custom command, this is now easy:

"You feel an immense sense of relief, as well."
achieve Nothing But Love
show adam grin
"Adam turns to you, grinning."

One caveat that I have not figured out yet: What to do if your achievement name contains quote characters. For example, I have an achievement called “Journey’s End” with an apostrophe in the name. Trying to grant it directly like this results in an error:

achieve Journey's End

Instead, I’ve special-cased that achievement, and grant it as JourneysEnd instead. I’m sure there’s a way to escape the apostrophe, but I also didn’t care to keep banging my head against it tonight.

One other important thing to note: These statements are notequivalent.

achieve Nothing But Love
achieve "Nothing But Love"

The version without quotes is almost certainly what you want. (I’m assuming the quotes are read literally, instead of denoting a string, but I also did not test to see exactly why it was failing to work properly.)

Codes: Achievements

I actually haven’t seen a full writeup of using RenPy’s achievements module. Thankfully, it’s super easy to set up, but I have no idea if this will actually integrate into Steam and such properly.

In any case: The game had collectibles. Yay. But I wanted some larger accomplishments (such as finding lots of collectibles) to be noted in a more prominent way.

The first thing to note is that RenPy’s achievements module is part of core RenPy and not its game scripts. (This is probably obvious, but I figured I’d point it out anyway.) As such, you have to call it from python code blocks.

To simplify things, I defined two kinds of achievements: Ones in a set list that are just boolean (you have it or you don’t) toggles, and ones that have a bit more complicated tracking logic. (I believe the latter will matter more if you actually integrate into Steam, but again, I have not actually tested any of this.)

First off: Defining your achievements. They just need a name, but I put a small description with them for UI display. Note the tuple instead of map, this time, so ordering is maintained:

python early:
basic_achievements = (
("First Steps", "Starting your college life", "Starting your college life"),
("Nothing But Love", "???", "Coming out to your parents"),
("Achievement Name", "Description when not unlocked", "Description when unlocked"),
…other achievements…
)

Then, you’ll want to register them with the backend. I believe this isn’t strictly necessary, but it doesn’t hurt to do.

init python:
for a, lockdesc, unlockdesc in basic_achievements:
achievement.register(a)
achievement.register("Growing Pains", stat_max=100, stat_modulo=1)
achievement.register("Compulsive Hoarder", stat_max=100, stat_modulo=1)

Note the manually-registered ones that include stat maxes. You could probably just register them with the actual number of collectibles, but I figured it’d be cleaner to deal with completion in terms of percentage, especially as I was still increasing the number of collectibles in the game.

Next, you actually want to grant these achievements in the game. This is, thankfully, straightforward.

"You feel an immense sense of relief, as well."
$ achievement.grant("Nothing But Love")
show adam grin
"Adam turns to you, grinning."

So this grants achievements (silently, to the player). How do you display them? I added them to the collectibles screen I defined in the previous post.

screen mycollection():
tag menu
default collectshow = "collectibles"
use game_menu(("Collection"), scroll="viewport"): vbox: spacing 10 hbox: spacing 20 textbutton ("Collectibles") action SetScreenVariable("collectshow", "collectibles")
textbutton _("Achievements") action SetScreenVariable("collectshow", "achievements")
if collectshow == "achievements":
for aname, lockdesc, unlockdesc in basic_achievements:
if achievement.has(aname):
text aname + ": {color=#777}" + unlockdesc + "{/color}"
else:
text "{color=#ccc}???: " + lockdesc + "{/color}"
if achievement.has("Growing Pains"):
text "Growing Pains: {color=#777}Finding " + str(int(len(collectitems)/2)) + " collectibles{/color}"
else:
text "{color=#ccc}???: Finding " + str(int(len(collectitems)/2)) + " collectibles{/color}"
if achievement.has("Compulsive Hoarder"):
text "Compulsive Hoarder: {color=#777}Finding all " + str(int(len(collectitems))) + " collectibles{/color}"
else:
text "{color=#ccc}???: Finding all " + str(int(len(collectitems))) + " collectibles{/color}"
else:
text "Found " + str(len(persistent.collection)) + "/" + str(len(collectitems)) + " collectibles" id "collectiblescount"
for k in sorted(persistent.collection):
if k in collectitems:
textbutton collectitems[k][0] action Show("itemdesc", itemid=k)

The last part is to increment progress as you acquire collectibles. This is also straightforward.

    def execute_collect(what):
if what in persistent.collection:
return
formatmsg = "{color=#090}{b}You found a collectible:{/b} "
if what in collectitems:
formatmsg += collectitems[what][0]
else:
renpy.error("Invalid collectable " + what)
persistent.collection.add(what)
totitemcount = len(collectitems)
curitemcount = len(persistent.collection)
if curitemcount >= (totitemcount):
if not achievement.has("Compulsive Hoarder"):
achievement.grant("Compulsive Hoarder")
else:
achievement.progress("Compulsive Hoarder", int(totitemcount100/curitemcount)) if curitemcount >= (totitemcount/2): if not achievement.has("Growing Pains"): achievement.grant("Growing Pains") else: achievement.progress("Growing Pains", int(totitemcount50/curitemcount))
if len(persistent.collection) == 1:
formatmsg += "\n(Access your collection from the main menu or pause menu. Collectible items will persist between games.)"
formatmsg += "{/color}"
renpy.say("", formatmsg)

As usual, I make no claims that this code is the best way to do things, or even a good way to do things. But it works. And, assuming achievements work the way I assume they will, doing it this way should also hook properly into things like Steam.

I was expecting the achievement framework to already put up some sort of notification, but it looks like RenPy doesn’t do this by default, so using this doesn’t really have any immediate advantages over directly writing persistent data.

This could be improved by adding a custom command to wrap achievement.grant() that can lint against the known list of achievements, and that can also show a notification to the player that an achievement was unlocked (some new screen that displays temporarily in some corner). This is perhaps an enhancement I will make at some point. 

Edit: Improved achievements, building on this code, is here.

Codes: Collectibles

Another thing I wanted to include in my game was little items that were both fun to collect, and also gave some indication of how much of the game you’d explored.

So the game has collectibles: Small named items that you acquire throughout the game that you can view descriptions of in a dedicated section of the menu. This isn’t a novel idea, by any means, but it also wasn’t entirely straightforward to set up. (Probably because I was really new to RenPy at the time.)

First, the core code that makes it work. There’s two sections here: First is the declaration of the actual items you can pick up, to keep it in one place, along with the associated persistent storage code:

init python:
if persistent.collection is None:
persistent.collection = set()
def merge_collections(old, new, current):
current.update(old)
current.update(new)
return current
renpy.register_persistent('collection', merge_collections)
python early:
collectitems = {
"adamid": ("Adam's Student ID", "A student ID for one Adam James Prewitt. It looks well-worn."),
"jalapeno": ("Jalapeno", "A whole jalapeno from dinner with Adam."),
…other items here…
}

Second, because I wanted to be able to easily grant these items as part of the game script, there’s a custom command that grants items. Again, note the Lint check, which is super useful to catch typos and missed sections when you rename items:

python early:
def parse_collect(lex):
what = lex.rest()
return what
def lint_collect(what):
if not what in collectitems:
renpy.error("Invalid collectible " + what)
def execute_collect(what):
if what in persistent.collection:
return
formatmsg = "{color=#090}{b}You found a collectible:{/b} "
if what in collectitems:
formatmsg += collectitems[what][0]
else:
renpy.error("Invalid collectable " + what)
persistent.collection.add(what)
if len(persistent.collection) == 1:
formatmsg += "\n(Access your collection from the main menu or pause menu. Collectible items will persist between games.)"
formatmsg += "{/color}"
renpy.say("", formatmsg)
renpy.register_statement("collect", parse=parse_collect, execute=execute_collect, lint=lint_collect)

This can then be straightforwardly called as part of your game script. The best part is that you don’t have to put it behind a conditional, because the player will only be notified if they don’t already have the item.

adam "Anyway…"
"He smiles at you before quickly walking out the door."
collect adamid
hide adam

So that grants collectibles, and tells the player when they receive a new one, but how to show the collection to the player? You have to make a new screen, which is thankfully straightforward. First, the screen that actually shows item descriptions, pulling from the map of valid collectible items you defined earlier:

screen itemdesc(itemid):
tag itemd
modal True
image "sp/itembg.png" at Position(xpos=0.5, ypos=0.5, xanchor=0.5, yanchor=0.5)
vbox at Position(xpos=0.5, ypos=0.5, xanchor=0.5, yanchor=0.5, xmaximum=1280, ymaximum=250):
text collectitems[itemid][1] id "collectitemtext" xalign 0.5 yalign 0.5 xmaximum 570 ymaximum 200
null height 10
textbutton "Dismiss" action Hide("itemdesc") xalign 0.5 yalign 0.5

Then the screen that lists the collectibles and makes them clickable to show their descriptions:

screen mycollection():
tag menu
use game_menu(_("Collection"), scroll="viewport"):
vbox:
spacing 10
text "Found " + str(len(persistent.collection)) + "/" + str(len(collectitems)) + " collectibles" id "collectiblescount"
for k in sorted(persistent.collection):
if k in collectitems:
textbutton collectitems[k][0] action Show("itemdesc", itemid=k)

Finally, add your screen as an item in the standard menu:

textbutton _("Load") action ShowMenu("load")
textbutton _("My Collection") action ShowMenu("mycollection")

Note that this doesn’t feed into the achievements system, but you probably don’t want it to… hundreds of achievements for every little thing in your game is probably overkill. But doing it this way allows the ability to easily add new collectibles (just add a new row to your map, and then collect itemname in your script) that persist between different plays of your game.