fuzzy notepadhttps://eev.ee/2025-07-21T20:56:00-07:00I am thirty-eight years old2025-07-21T20:56:00-07:002025-07-21T20:56:00-07:00Eeveetag:eev.ee,2025-07-21:/blog/2025/07/21/i-am-thirty-eight-years-old/<p>There are several old, personal events that I’ve been rotating in my head for a very long time. I’m finally writing about them because I’ve just had the staggering realization that they all form <em>one</em> singular story. In some cases I’d never made the connection; in other cases I just plain forgot that things which happened within <em>hours</em> of each other were related.</p>
<p>This isn’t pleasant to write, and it won’t be pleasant to read. But I need it out of me.</p>
<p>I might have some of the details wrong, since I’m piecing together fragments from decades ago. This is a story, not a documentary. It’s about me, no one else.</p>
<p><strong>content warning:</strong> underage sex; the active pursuit thereof by adults; bestiality mention.</p>
<p>There are several old, personal events that I’ve been rotating in my head for a very long time. I’m finally writing about them because I’ve just had the staggering realization that they all form <em>one</em> singular story. In some cases I’d never made the connection; in other cases I just plain forgot that things which happened within <em>hours</em> of each other were related.</p>
<p>This isn’t pleasant to write, and it won’t be pleasant to read. But I need it out of me.</p>
<p>I might have some of the details wrong, since I’m piecing together fragments from decades ago. This is a story, not a documentary. It’s about me, no one else.</p>
<p><strong>content warning:</strong> underage sex; the active pursuit thereof by adults; bestiality mention.</p>
<h2 id="i-am-five-years-old"><a class="toclink" href="proxy.php?url=#i-am-five-years-old">I am five years old</a></h2>
<p>My family moves from the <span class="caps">UK</span> (where my mother is from) to an American military base elsewhere (as my father is in the <span class="caps">US</span> military). In the switch from the <span class="caps">UK</span> to <span class="caps">US</span> school system, my parents push to have me put in second grade, on the grounds that I’ve been absorbing basically anything I’ve been exposed to since I was old enough to walk, and I’d be bored to tears in kindergarten.</p>
<p>This puts me two grades ahead for my age, which makes me two years younger than everyone around me, which will remain the case until I graduate from high school. I’m still quicker on the uptake than most everyone in my grade, and later get shifted a <em>third</em> year ahead in math. I never have school-age peers. This is normal.</p>
<h2 id="i-am-eleven-years-old"><a class="toclink" href="proxy.php?url=#i-am-eleven-years-old">I am eleven years old</a></h2>
<p>I’m a picky eater. A lot of foods actively repulse me. My mother keeps making them for dinner anyway. I do my best to eat around them. Once she makes a quiche, and the taste of the cheddar makes me instantly want to vomit, so I can’t eat any of it. My father insists I sit at the table until I’m finished. I try a few bites but can’t bear it at all. I sit there for an hour, alone, before he gives up and lets me slink away.</p>
<hr />
<p>I like computers. I don’t know much I can do with them besides toodle around in QBasic, but being able to write out instructions and have a <em>thing</em> happen feels like magic to me. I’m enamored. I’ll stay enamored for the rest of my life. I’m dimly aware that Windows and Office are also software, but they seem too incomprehensibly vast and complex to have been <em>made</em>, let alone made by fundamentally the same process I’m engaging in when I draw circles on the screen. I don’t consciously think about this, merely take for granted that they emerged fully-formed from a Company, which is somehow a different sort of entity from a person.</p>
<p>I think the Internet sounds cool but I don’t really know what there is to do on it besides download utilities I don’t need or read about The Microsoft Windows 95 Product Team! easter egg, which I only ever get to work once. I also find out that you can trick <span class="caps">MS</span> Paint into taking a screenshot of its own help window, which is cool because I don’t know how to take actual screenshots. That means I can make fake UIs, which is cool because I don’t know how to make real UIs, and I don’t know how to draw, either. Art, too, seems like some kind of foreign magic.</p>
<p>I’m really into Animorphs. I want to turn into a red-tailed hawk like Tobias and just fly away. I’m starting to do less well in school, and feel a budding hostility coming from my parents over it. I don’t have a lot of friends, don’t really have a sense of how to make them, and don’t think about it much. I feel a little out of place everywhere, but I always have, so it’s normal to me.</p>
<p>I hear about book 16, The Warning. It’s the one with Jake morphing into a rhino on the cover. I haven’t read it yet, but as I understand it, the plot centers around one of the protagonists typing “yeerk” or something into a search engine and finding exactly one result, which they then go investigate.</p>
<p>I think about this. I know about the Internet and search engines. But obviously, I think, entering “Yeerk” wouldn’t find anything, because Yeerks aren’t real. I try it anyway, just to see. I’m stunned to discover the world of fansites.</p>
<p>One of them has a forum and even a chat room attached. I join both and am stunned once more to discover that the Internet has other people on it, just hanging out. The other people are all teenagers, a little older than me, but I’m used to that. Half of them also have overbearing parents, and we bond over bitching about them. I can be kind of weird and awkward here and it’s fine. I’m really happy about having found this little sanctuary, and I start spending a lot more time online.</p>
<h2 id="i-am-twelve-or-thirteen-years-old"><a class="toclink" href="proxy.php?url=#i-am-twelve-or-thirteen-years-old">I am twelve or thirteen years old</a></h2>
<p>I’m in ninth grade. My parents have put me in a private school, and it is fucking miserable. Homework is so tedious it feels akin to torture, so I just don’t do it, so my grades drop, so I get endlessly scolded and told I’m a disappointment. Chores, too, are agonizingly boring, and my mother regularly screams at me for not doing the dishes. None of the adults in my life — not parents, not teachers, not other school staff — suspect I have <span class="caps">ADHD</span>, perhaps because I’m smart and quiet, and I will eventually work it out myself some years later. Everyone else seems to believe <em>their</em> lecture will be the one to finally inspire me. My parents, who had once fought to save me from boredom, don’t recognize it happening in front of them.</p>
<p>I’m miserable at home from all the screaming, which makes me even more reclusive and less interested in school, which makes my grades all the more mediocre, which makes my parents yell more, which makes me more miserable.</p>
<p>Perhaps luckily, I don’t draw any conscious conclusions from any of this. I have no sense of how other people experience the world, and I haven’t really thought about, say, whether homework is easy for <em>other</em> people. I don’t even understand that I’m struggling, because I have nothing to compare it to. I don’t remember being a little kid very clearly, so as far as I can tell, it’s just always been like this. This is normal.</p>
<p>I have a little breakdown once and yell back at my mother, trying to convey… why I’m unhappy, without fully understanding it myself. She stands there, stunned. My father storms into the room, grabs me by my shirt collar, drags me upstairs to my bedroom, and throws me into it. He gets a utility knife and cuts through several random cables on my computer, then leaves without a word.</p>
<p>One of the cut cables is my keyboard, so to use my computer, I have to steal the keyboard from <em>his</em> computer and be sure to return it before he gets home and notices. Otherwise I would be completely isolated.</p>
<p>I learn a valuable lesson. Adults will hurt me, and this is normal. I hurt quite often, but I can’t do anything about it, and if I try, adults will hurt me more, so I just sit with it.</p>
<p>Sometimes I used to cry, but then my mother would hear and come tell me (in a caring voice) not to, because I’d give myself a headache. I took that to mean I just shouldn’t, so I’ve stopped.</p>
<p>My parents will later try to send me to a therapist a couple times — the problem is of course with me, not them, never them. I confide the encounter with my father, which makes it through some unseen grapevine, and I end up having to talk to some sort of military-<span class="caps">HR</span> person about it. Fearing that I might get put into the foster system and things will somehow end up worse, I lie that I had it coming. I hate lying, but I’ve learned that I have to lie to adults sometimes, so they won’t hurt me as much.</p>
<p>It isn’t mentioned again. My parents never say a word to me about it… until over a decade later, when my mother will tell me that I was physically imposing and physically threatened her. I will have no idea what she’s talking about — until that moment, the thought of attacking her in some way never crosses my mind. I’ll also be a late bloomer, insofar as I’ll bloom at all, and one of the few strong images I’ll remember from that day will be my mother <em>looking down</em> at me. But she will remain absolutely convinced that I was a threat, and that is why my father took the therefore-fully-justified actions he did, and I will be unable to disabuse her of this notion up through the end of her life. One day, many years later, she will die of cancer, having never believed me about my own motivations.</p>
<p>She will also, in the same conversation, chide me for not doing the dishes. I will be almost thirty years old.</p>
<h2 id="i-am-fourteen-years-old"><a class="toclink" href="proxy.php?url=#i-am-fourteen-years-old">I am fourteen years old</a></h2>
<p>I’m in tenth grade, taking <span class="caps">AP</span> calculus. I’m good at it, but the homework is still mindnumbing.</p>
<p>I try to coast through my own life, attracting as little attention as possible from the adults around me who have the power to hurt me. I’m not fully successful. But when I’m hurt, it’s normal.</p>
<hr />
<p>I’m still online a lot. I’ve gotten into doing, well, “web stuff”. It started out with posting little JavaScript snippets onto a small forum that doesn’t strip it out, or using a lot of <code><font></code> tags to make rainbow text. I’ve also gotten into Pokémon, and I feel a strong affection for tables and lists, so I start to make a Pokédex website. I don’t really know what I’m doing, and much of the effort comes from painstakingly retyping information from strategy guides or just other people’s websites, a process my future self will find comically rudimentary in hindsight. But it still feels like magic, and now I can share it with other people, too. I don’t know if anyone uses my website, but I’m delighted to have made it.</p>
<p>I’ve also hit puberty — several grades after everyone else, which has been a little awkward — and am starting to hear about this “sex” thing. It sounds pretty interesting. I end up combining my interests and joining an <span class="caps">IRC</span> channel dedicated to Pokémon porn. I’m probably the youngest person here, but no one cares, and I have no sense that there’s any reason anyone <em>would</em> care. There are some older teenagers here, as well as some adults, ranging all the way up to one 40-year-old — but he’s a completely regular cheerful guy who just genuinely enjoys writing fics about Sabrina having sex with an Alakazam or whatever.</p>
<p>But there’s also a guy who makes the occasional comment about “little girls”. There are at least one or two people who casually mention they have regular sex with their dogs. No one bats an eye at this, so I don’t, either. I have no basis for comparison, because I am fourteen years old. Maybe this is normal. Everyone else acts like it’s normal. It must be normal.</p>
<p>Sometimes people try to have cybersex with me. I’m not very good at it. I don’t really know anything about sex, but I start to pick it up from how other people describe it. It’s fun to write about this thing I’ve never done, this activity so mysterious that it almost feels like it must itself be fictional. It feels like it only exists in a bubble, completely detached from normal life.</p>
<hr />
<p>Offline, I still barely know anyone. I’ve sort of gravitated to a couple other nerds at school, but outside of the fact that we are all vaguely aware how to make a website, we don’t have a lot in common. One of them is just kind of mean, even. This is normal. I’m two years into high school and just barely hitting the age when most people are starting it. I live in Hawai‘i at the moment, and almost everyone else has lived here their whole lives, but I’ve never even been to the same school for more than two years.</p>
<p>I find out about a little old-school website where furries can enter their location and find other furries nearby. I put in my zip code. Nobody else, it seems, lives in Hawai‘i.</p>
<h2 id="i-am-still-fourteen-years-old"><a class="toclink" href="proxy.php?url=#i-am-still-fourteen-years-old">I am still fourteen years old</a></h2>
<p>We move, for the fourth time in my life, this time to the <span class="caps">US</span> mainland.</p>
<p>I update my zip code on the furry location website. Still nothing.</p>
<p>But then, out of nowhere, I get a message from someone I don’t know, who I’ll call 🐨. He’s eighteen, four years older than me, but that’s normal. He says he used to live in my town and he’s passing through for just a day or two, and would I like to meet up? I’m fucking ecstatic and say yes.</p>
<p>My mother drives me to where he’s staying. It has that 1970s wood panelling everywhere, which I might be seeing for the first time. It ultimately leaves me with a strange, otherworldly impression.</p>
<p>We talk a bit, and then he clearly wants to have sex. This hadn’t come up in our brief conversations beforehand. He seems surprised, but unswayed, that I haven’t had sex before. I don’t see any reason to turn him down — sex is supposed to be The Best Thing, after all.</p>
<p>We fool around some. It’s… fine. I don’t really like how he touches me. But hurting is normal, and this barely hurts at all, so I don’t say anything. I don’t even know how to say anything. People don’t show much interest in what I want. If anything, what I want seems to be an inconvenience to everyone else.</p>
<p>So I don’t say anything. It’s fine. This is normal.</p>
<p>Things peter out. I go home.</p>
<p>I’m no longer a virgin. It seems like something should be different. But nothing is. I don’t really think about it.</p>
<p>I try to keep in touch with 🐨, but he isn’t around much. He’s part of a little group of furries who all live in the same town and know each other, though, and they start to reach out, and I talk to some of them.</p>
<h2 id="i-am-sixteen-years-old"><a class="toclink" href="proxy.php?url=#i-am-sixteen-years-old">I am sixteen years old</a></h2>
<p>[Hello, future Eevee here. Just letting you know, this is your last chance to back out. –ev]</p>
<p>I’ve just graduated high school. I’m so close to being away from my parents, to living on a college campus in a distant state. It’s exhilarating, but also terrifying, because I don’t really know how to live on my own. I’ve never done laundry or bought my own food. I don’t have a car or much money. I don’t really know how to do anything, other than make websites that look like they were made by a sixteen-year-old.</p>
<p>Over the past couple years, a number of guys have shown sexual interest in me. Almost all of them have been eighteen or older. I’ve met some of them at furry conventions and had sex with them. I didn’t really like any of it. But I’m desperately starved for affection and still assume the problem is with me, so I keep taking any opportunity I’m given. Maybe the next time will be better? I don’t know what else to do, so I keep doing what I’m doing.</p>
<p>I’m sufficiently self-aware of this inner turmoil to post about it. The only relevant comment I get is from someone I do not know and never otherwise speak to.</p>
<blockquote>
<p>There is absolutely nothing wrong with giving it up for whoever wants it, especially at your age!</p>
</blockquote>
<p>I am sixteen years old. This is normal. It can only be normal. No one else thinks anything of it, so I don’t either.</p>
<p>I attend another furry convention not long before I’m to move into a college dorm. My family’s situation is a little complicated at the moment — the house has been sold, my mother is in an apartment in our old town, my father is in an apartment in the new town, I’m off to a convention, and somehow this is all intended to coalesce later.</p>
<p>I have two sexual encounters that have… <em>ramifications</em>.</p>
<hr />
<p>One is with 🐯, who I met somehow-or-other through 🐨’s group, despite not being local to them. [I have no memory whatsoever of how we met, why we started talking, or what we talked about. –ev]</p>
<p>He is twenty-six years old, a full decade my elder. He is openly interested in me because I’m underage. This is normal. After all, I <em>am</em> underage, and most of the people capable of travel are adults, so anyone who would have sex with me would at the very least have to find it <em>acceptable</em> that I’m underage.</p>
<p>We meet up at this con. He has sex with me. As usual, I don’t really know why I’m participating.</p>
<p>It’s the worst sex I will ever have in my life, deeply unpleasant and uncomfortable. I spend every single moment of it desperately wishing for it to be over, but I don’t know how to ask him to stop. I expect people to hurt me if I push back against what they want from me, but I’m not even cognizant of this — I see myself as just wanting to make people happy. Eventually I can’t take it any more and, in a flash of inspiration, offer to fellate him instead. I don’t really care for that, either, but it’s much less bad.</p>
<p>He gets me to promise I won’t tell anyone. I’m vaguely aware that this is the sort of thing he shouldn’t be doing, and I don’t want anyone in trouble on <em>my</em> behalf, so I agree.</p>
<hr />
<p>There’s also 🐸, who I’m at least acquainted with, though we’re not exactly close. We hang out in a couple of the same <span class="caps">IRC</span> channels and have friends in common. Also, we’re the same age, almost exactly — we were born in the same month.</p>
<p>We also meet up and have sex. This time, at least, it seems like sort of maybe a good idea. At least it’s someone I <em>know</em>. It’s not great, but it’s not nightmarish, either.</p>
<p>He leaves his phone in my hotel room. I happen to catch a glance of him a little later, and so I run up to him to return his phone.</p>
<p>His father is with him, and is furious. He’s absolutely convinced I’m some kind of sex predator, despite that we’re exactly the same age and I look <em>younger</em> than 🐸. I go for my wallet but he sense my intentions and angrily insists he doesn’t care what kind of <span class="caps">ID</span> I have. He declares he’s placing me under citizen’s arrest, a thing I’ve never even heard of. But of course, I believe I have to go along with adults, or they’ll make things even worse.</p>
<p>He actually calls the police, who spend about two seconds checking my <span class="caps">ID</span> and say “yeah this is fine”. But then they want me to Make A Statement Down At The Station, so I go there, and I awkwardly describe a bland teenaged sexual encounter to someone who is a remarkably slow typist considering it seems to be their whole job.</p>
<p>And now I’m at a police station, and the police only want to release me into my parents’ custody, because I am sixteen years old. So they call my father, who is thankfully only a few hours’ drive away. And they put me in a chair and tell me that if I get up they’ll lock me in a cell. And I sit there, for two hours, while cops twenty feet away crack jokes with each other about the fact that two teenagers fucked. It may have been more or less than two hours, but I have undiagnosed <span class="caps">ADHD</span>, which has a way of stretching out activies like sitting in a chair doing nothing.</p>
<p>My father arrives, so silently furious that he accidentally drives into the wrong state on the way back to his apartment. He demands I log into my laptop, and he changes my password. Once I’m alone, because he’s off at his job as some sort of network administrator, I log into my laptop as admin, and change my password back. [Bright spot in this story. Fucking hilarious. Great job, li’l Eevee. –ev]</p>
<p>I then write a public post about the experience, which ends up linked on a now-defunct drama site. A bunch of people — who are we kidding here, <em>more adult men</em> — have a grand laugh about, again, two teenagers having sex. It probably doesn’t help that the post is written in an almost painfully cutesy affect, since I am sixteen years old. Several dramamongers approach me personally to be nasty, including one who calls me a “sick fuck” for “doing kids”. I am sixteen years old.</p>
<p>One of the convention staff also emails me with a brief rant, asking why I’m trying to destroy the convention by writing about things that happened to me, because now he’s fielding accusations that the con is full of pedophiles (presumably, again, because I had sex with someone my age). I have no idea what to say to this and never reply.</p>
<p>I do show it to 🐯, hoping for support. I happen to think that it’s absurd to blame someone for posting that they had sex at a con. But 🐯 insists I’m wrong and should apologize. I deflate.</p>
<p>My father later talks to me about the event. The conversation is extremely one-sided, because I know what happens if I push back against anything. He tells me I’m cold, calculating, manipulative, evil. He tells me I care only about myself. That I have no soul. That he doesn’t want me in the house.</p>
<p>I am sixteen years old.</p>
<p>All of this is normal.</p>
<hr />
<p>The irony is, unfortunately, lost on me — because as requested, I erased mention of 🐯, the twenty-six-year-old who had sex with a sixteen-year-old, from my story. I erased it so thoroughly that I will forget these two encounters happened on the same weekend until <em>many</em> years later, even as I will continue to be lightly haunted by a memory of horrendous sex I felt trapped in.</p>
<p>Sometime in the next week and a half, I admit to <em>someone</em> that I had sex with 🐯. [I don’t know who, but I think I was pointedly asked, and I didn’t really know how to reject questions, <em>and</em> I’ve never liked lying, so I can extremely see how I would end up just saying it. –ev]</p>
<p>This makes it through some unseen grapevine, and suddenly 🐯 is furious with me, threatening to end the friendship [lol –ev] unless I fix it somehow, by convincingly lying to someone in this gossip chain that I don’t know. I make a half-hearted attempt, which I hate, and am (unsurprisingly) not believed.</p>
<p>Our relationship, such as it is, deteriorates, both because 🐯 himself deteriorates and because I don’t seem to have as much interest in trying to be friends with the person I had inescapable nightmare sex with. I must feel resentful of him without ever wanting to confront him directly, because I will later discover a few remaining scraps of one of our last conversations:</p>
<blockquote>
<p>🐯: Gods eevee you’ve become such an annoying little bitch, I can’t beleive I was ever even nice to you. I wouldn’t have come within 20 feet of you had I known you were this kind of person.</p>
</blockquote>
<p>I am sixteen years old. I am being spoken to by a twenty-six-year-old man.</p>
<blockquote>
<p>🐯: gods, you and your stupid faces</p>
</blockquote>
<p>I am sixteen years old, and I use emotes as punctuation o.o to a ridiculous degree ^o.o^ like multiple times per line o.o and the twenty-six-year-old man who was so eager to have sex with me is now sick to death of how juvenile I am. If only there were some way he could have foreseen this.</p>
<p>I am sixteen years old, but I begin to realize I do not give a shit about this loser who can only bed teenagers, nor about his big important opinion of me. He’s mad at me, but it doesn’t matter. Adults have been mad at me my entire life. What’s he going to do, type at me? I glaze over. I become laminated. I rebuff everything.</p>
<p>He only talks to me once more, to say he misses seeing me around. I don’t care.</p>
<p>I am sixteen years old. I start to wonder if this isn’t normal.</p>
<h2 id="i-am-eighteen-years-old"><a class="toclink" href="proxy.php?url=#i-am-eighteen-years-old">I am eighteen years old</a></h2>
<p>Someone new joins the Pokémon porn <span class="caps">IRC</span> channel. They are fifteen years old. I don’t think anything of it, just as no one thought anything of it when I first entered. This is normal. Sort of.</p>
<p>I recognize their name from the artwork that decorates several Pokémon fansites. I find it fascinating that they were able to create any of that. It’s like magic to me.</p>
<p>There are a few artists here already, but this is the first whose art was truly captivating to me. Somehow it feels more impressive yet also more real, like I can believe it was done by a person. It plants the tiniest seed that maybe, one day, I can do it too.</p>
<p>I approach them to say hi, that I like their art. We have an actual conversation, then another. It’s like a breath of fresh air. So many people I’ve talked to have just wanted to hit on me way past the point of comfort and barely have a personality beyond that. But nothing like that happens here.</p>
<p>Instead we talk about actual <em>things</em>: Pokémon, and art, and our lives, and all the wrinkles they’ve had so far. They like cats. I like puzzles. Sometimes they struggle with pressure from overbearing commissioners, and something about that must resonate with me, so I try to be supportive. Later I’ll admit I’m still struggling with affection and my inability to tell people no, and they’ll be supportive of me, too.</p>
<p>It’s nice.</p>
<p>One day, it’ll even be normal.</p>
<h2 id="i-am-thirty-two-years-old"><a class="toclink" href="proxy.php?url=#i-am-thirty-two-years-old">I am thirty-two years old</a></h2>
<p>I’m at the <span class="caps">DMV</span>. My best friend, someone I met a lifetime ago — in a Pokémon porn chat, of all places! — is here with me.</p>
<p>We live together, now, with our five cats, and we’ve recently escaped someone we both struggled to push back against. It feels like a small victory, but it was hard-earned.</p>
<p>We both sign the marriage certificate.</p>
<h2 id="i-am-thirty-eight-years-old"><a class="toclink" href="proxy.php?url=#i-am-thirty-eight-years-old">I am thirty-eight years old</a></h2>
<p>I’m thinking back on a lot of things. It’s almost dizzying to see so many little threads of causality. My parents, even teachers, practically training me to think that whatever <em>other people</em> want is paramount. The deeply fucked-up culture of early-00’s Internet, where people could just openly announce their interest in doing sex crimes and no one batted an eye. Even the notion of a 14yo in a space dedicated to porn sounds unthinkable by today’s standards, but I poked my head in a lot of sex-themed places back in the day and <em>not one of them</em> cared how old I was.</p>
<p>I suppose I was well-spoken enough to sound older (aside from the hailstorm of o.o), but at the same time my <em>social</em> development was… almost non-existent. Hence how I had 20-somethings talking to me like I was an equal, all while I didn’t even understand how to say “I don’t like this”.</p>
<p>It took me a few more years to extricate myself from the weird little rut I’d dug for myself. It certainly helped that, around nineteen or twenty, <em>vastly</em> fewer random older men were interested in me. I’ll just, uh, try not to think too hard about that.</p>
<p>I don’t know what would have helped me avoid this. I keep thinking back to the vague ambient warnings about the Internet in the early 00s, which mainly focused on how anyone might be <em>lying</em> to you, might be <em>pretending</em> to be your age to trick you into sex later.</p>
<p>But that never happened to me. It was so unlike my experience that it almost feels laughable. Everyone I had sex with was pretty open that they wanted to have sex with me, and I agreed. No one ever warned me that sex without pretense could have <em>emotional</em> consequences. Everything in my (regular, offline) life that tried to tell me anything about sex was laser-focused on either pregnancy, STIs, or a guy in a van offering me candy. Like, hello, I was a deeply lonely sixteen-year-old. They didn’t need to offer me candy. They just offered me sex!</p>
<p>And there <em>are</em> lingering consequences — although now that I’m happily married and no longer on the radar of a bunch of people who really want to sleep with a teenager, they largely don’t matter in practice. But I had so much terrible, uncaring sex with men that I feel a little anxious even considering the thought of doing it again. There’s no one besides my spouse who I <em>want</em> to have sex with at the moment, but I still don’t like having that stuck in me. Like a shackle around my ankle that isn’t chained to anything, but it’s still <em>there</em>, and occasionally I feel it rattle.</p>
<hr />
<p>But what really struck me, what <em>really</em> compelled me to write this down, was the realization of a strange pattern in the post-con sequence of events.</p>
<p>I think it’s fair to say that 🐯 used me for sex. I played along, but I think there’s at least a <em>little</em> bit of a responsibility gradient here.</p>
<p>But then, wait. Some group of people confirmed with me that I’d had sex with 🐯, and then I guess started gossipping about it, possibly even harassing him. Do you know how many people from that circle reached out to me, to see how <em>I</em> was doing?</p>
<p>Zero. Nada. I was useful only as long as it took to crystallize a nugget of Drama™, and then I was no longer needed.</p>
<p>So let me recap, this time with some editorializing:</p>
<ul>
<li>A man ten years my elder used me for sex.</li>
<li>A bunch of adult men used me for laughs.</li>
<li>Some kind of gossip ring used me for, well, gossip.</li>
<li>A con staff member used me to vent about something that, frankly, furry conventions seemed to deal with a lot in the 00s.</li>
</ul>
<p>Not <strong>one</strong> of these <em>many</em> adults reached out to see if <em>I was okay</em>. The con staff guy didn’t know about 🐯, of course, but they <em>did</em> know I’d had a harrowing experience and now was having at least one more — because those are what my whole fucking post was about! — and yet the only reason they went through the effort to find my email and reach out was to blame me for it <em>again</em>.</p>
<p>But it’s the gossip ring that I truly cannot excuse. The <em>sole reason</em> there was any gossip to be had <em>at all</em> was the idea that a twenty-six-year-old having sex with a sixteen-year-old is, in some sense, <em>bad</em>. But this clearly didn’t actually <strong>mean</strong> anything to them! It was “bad” only in the abstract, “bad” only in the sense that it gave them an excuse to ostracize the “bad” person, or laugh, or whatever the fuck they were doing.</p>
<p>It’s no different than that drama-site clown calling me a “sick fuck for doing kids” or whatever the hell. You could not possibly read a post about how I <em>had to wait for my dad to pick me up because the cops wouldn’t release a minor</em> and not grasp that <em>I am a minor</em>. Like, I <strong><span class="caps">AM</span></strong> “<span class="caps">THE</span> <span class="caps">KIDS</span>”! You, my fucking guy, <em>right now</em>, are being cruel towards the people you’re feigning concern for! But it just didn’t matter what happened or who was involved or who was hurt by it. Some asshole — almost certainly yet another adult — just wanted to be nasty, and they thought they saw someone they were allowed to be nasty to, so they were.</p>
<p>None of these people were interested in helping a sixteen-year-old. They only wanted to lash out at someone. The best I got was a tiny apology from 🐯, of all fucking people, who eventually caught on that I had not <em>fully</em> enjoyed our time together. But he can, of course, shove that entirely up his ass.</p>
<hr />
<p>For many, many years, I’ve avoided making any mention of the thing with 🐸, my first exposure to the Internet “Drama” Circuit. I feared it would happen again, or that I’d be called a pedophile some more by people who just conveniently forget that we were the same age. I’d completely forgotten that 🐯 happened at the same time — because he’d basically asked me to detach him from the rest of it! Rediscovering that little tidbit has sure cast this story in a different light.</p>
<p>But like, fuck that, regardless? I will talk about my own life in whatever goddamn way I please. As soon as I decided to write this down, I couldn’t even remember why I’d ever been scared to do it. I guess I <em>had</em> been pretty thoroughly punished for writing it down the <em>first</em> time.</p>
<p>And sure, with decades’ worth of hindsight, it was perhaps not a <em>good</em> idea to have described my underage sex life — or the brief entanglement of the police with it — in public. But I still reject the idea that it was <em>wrong</em> to do so, or that any subsequent ragging on the convention was <em>my fault</em>. The actual story here (once 🐯 was stripped from it) was that Some Fucking Guy overreacted and <em>called the cops</em> because his teenaged kid got laid and he didn’t like that. That is fucking <em>bananas</em> behavior for a grown-ass man, but somehow fingers ended up pointed at literally everyone else. Clown world.</p>
<hr />
<p>And various people have been calling me a pedophile ever since anyway. I’m often not privy to why. Like, as best as I could discern, the Something Awful Pokémon crowd branded me a pedo at one point because I had some cutesy, non-sexual, unremarkable artwork <em>of myself</em> (i.e., an Eevee person) as the background of my website for a while. Like that’s it, that’s the whole thing. Conspicuously, <strong>I am not attracted to, or otherwise interested in, teenagers or children</strong>, but that just doesn’t seem to factor in. You’d think it would be kind of important, right? But there’s this weird chain of semantic implications that lets you suggest someone <em>actively molests children</em> based purely on <em>vibes</em>, without ever having to identify any concrete child, and that seems kind of bad to me, but if I try to explain it I’ll probably be called a pedophile, because why would anyone but a pedophile defend pedophiles by nitpicking the definition of “pedophile”, huh?</p>
<p>Meanwhile, I was actively pursued by much older adults! 🐯 isn’t even the oldest guy who had sex with me <em>when I was sixteen</em>! But I’ve spent half a lifetime nervous about even admitting that, out of some nebulous fear of the reaction, all while I get lumped in with the sort of people <em>who did it to me</em> because my website background doesn’t have a suit and tie or what the fuck ever. What a joke.</p>
<p>It makes me feel fucking crazy, sometimes, to watch our culture obsess over rooting out anyone with a whiff of “pursues sex with a minor” with the same furor and accuracy as we once rooted out people possessed by Satan, but with “the minor” — a <em>person</em> — reduced to a sort of… fantasy hypothetical? Or just dropped entirely, I guess. “Pedophile” is the thing you call someone that makes you win, because that’s the worst thing, and they can’t prove you wrong. Even the richest man in the world does it.</p>
<p>Sometimes I think about what might happen in another timeline, where I’m sixteen <em>now</em> and I post this story. I’m sure 🐯 would be absolutely roasted right off the Internet — but how many people would still check <em>on me</em> for anything other than more sordid details?</p>
<p>…But then, who have <em>I</em> checked on? How many times have I had the opportunity, and not taken it?</p>
<p>I can definitely think of one or two. But that’s a whole other rabbit hole.</p>
<hr />
<p>This sucks. I feel like basically every adult in my teenaged life let me down, and I have no idea what to do with that information.</p>
<p>I guess all I can do is try to reach back in time with the power of blogging and say what I desperately needed to hear.</p>
<p>If <em>you</em> are a teenager reading this — I don’t know how or why, but I am functionally powerless to stop you — and even a little bit of it has resonated with you, then let me impress upon you this: <strong>how you feel <em>matters</em></strong>. Even if it doesn’t seem to matter to the people around you, the people with power over your life, it should still matter <em>to you</em>. Hold onto it, even if you have to hide it, and do not let go for anyone.</p>
<p>I’m sorry for whatever you may feel trapped in. I’m sorry if it’s hard. It might keep being hard for a little while. But if you keep looking, you <em>will</em> find people who care about what you want, who will have your back when you struggle to stand up for yourself, and who won’t punish you for hurting.</p>
<p>Please take care of yourself.</p>
<p>P.S.: Sex is an <em>amplifier</em>, not an automatic good time. It’s like Mario Party: a hilarious chaotic mess with the right people, but a horrible fucking slog with the wrong people.</p>
<hr />
<p>I am thirty-eight years old.</p>
<p>I still think about what happened to me when I was sixteen. Not all the time. But sometimes.</p>
<p>Maybe after today, I can finally stop.</p>The rise of Whatever2025-07-03T17:26:00-07:002025-07-03T17:26:00-07:00Eeveetag:eev.ee,2025-07-03:/blog/2025/07/03/the-rise-of-whatever/<p>This was originally titled “I miss when computers were fun”. But in the course of writing it, I discovered that there is a <em>reason</em> computers became less fun, a dark thread woven through a number of events in recent history.</p>
<p>Let me back up a bit.</p>
<p>This was originally titled “I miss when computers were fun”. But in the course of writing it, I discovered that there is a <em>reason</em> computers became less fun, a dark thread woven through a number of events in recent history.</p>
<p>Let me back up a bit.</p>
<h2 id="bitcoin"><a class="toclink" href="proxy.php?url=#bitcoin">Bitcoin</a></h2>
<p>Back in the 00’s, if you wanted to move money between arbitrary people over the Internet, you realistically had one option: PayPal.</p>
<aside class="aside--well-actually">
<p>Either that, or live in some futuristic utopia like the EU where banks consider "send money to people" to be core functionality. But here in the good ol' U S of A, where material progress requires significant amounts of kicking and screaming, you had PayPal.</p>
</aside>
<p>The thing about PayPal is that it holds onto your money, but it isn’t a bank? I do not fully appreciate the architecture or its implications here, but PayPal’s point of view seems to have always been that they can do whatever they want. They’ve always been pretty fussy about the use of PayPal to facilitate commissioning artists for drawings of unicorn wieners, for example, so if they thought you were doing that, they would just lock your account and also keep all your money for six months. For safekeeping, I guess. And interest.</p>
<p>Yet PayPal was the only option for many rinky-dink individuals selling one-off goods and services, so there was some amount of frustration that the <em>only available middleman</em> had exclusive right to say how you were allowed to spend your money, or what kind of indie business you were allowed to run. And if they caught you ignoring the rules then they got to keep your money for half a year.</p>
<p>And then in 2010 or so, I heard about Bitcoin. And it sounded like the wave of the future. Finally, a way to just <em>send money to someone</em>. What a fucking concept. And imagine what you could build with such a system! Websites could have real tip jars. Browsers could have tipping built right in that transfer only a few cents, since transactions would be so effortless.</p>
<p>I downloaded a miner (well, <em>the</em> miner at the time, I think) and ran it for like a day and failed to mine a coin. There was nothing else to really do, so I closed it and forgot all about Bitcoin.</p>
<p>Fast forward a bit, Bitcoin has reached mainstream awareness, and… none of that stuff happened. Bitcoin is not so much a currency as it is an entire <em>ecosystem</em> of schemes. The only mention I’ve heard in the last year of being able to actually <em>buy</em> anything with Bitcoin was gray market estradiol. (Even gray market <span class="caps">FIP</span> medication just takes credit cards!) The only browser with built-in tipping is the one spearheaded by a man whose other claims to fame are inventing JavaScript and wanting to outlaw my marriage, and the token it uses apparently had 80 whole sellers in the past 24 hours. Sounds like all of that is going great.</p>
<p>Meanwhile, fifteen years later, the state of the art in sending arbitrary people money seems to be… uh, PayPal. But now we have Stripe, too, which can take credit card payments if you know how to make a website that uses it, but which <em>also</em> forbids drawings of unicorn wieners. Patreon? Stripe and PayPal. Itch? Stripe and PayPal. Ko-fi? Stripe and PayPal. Nothing is fundamentally different.</p>
<p>But the dream has died. It almost came true, and then it was immediately co-opted by a bunch of get-rich-quick grifters and a bunch of turbo-libertarians whose entire identities are defined by the Things that they Own and who want to cryptographically impose that on everyone else too because they’re mad that World of Warcraft nerfed warlock or something.</p>
<p>And I suspect the core problem that has wended its way through the history of cryptocurrency is that the vast majority of people involved <em>do not actually care</em> what the thing they’re flocking to is. What they care about is that it has a graph, and that they get rich if the graph goes up, so they say whatever might make the graph go up. The graph even looks exactly the same for every coin and <span class="caps">NFT</span> and Whatever else: x-axis time, y-axis dollars. The only place the <em>thing</em> appears at all is in the title, where you can safely ignore it.</p>
<p>Plenty of people will talk up the supposed benefits of their pet thingamajig, of course, but my suspicion is that many of them don’t actually care that much. They have a <em>vested interest</em> in getting other people to buy into the thing, Whatever the thing may be, because then graph go up.</p>
<p>And so you have what I can only call a culture of <strong>Whatever</strong>. Bitcoin failed as a currency because the people who got most invested in it <em>do not care</em> about currency — it could be bottled dragon farts for all they care, except that putting it on the computer means there’s no need to actually worry about a product. It’s just something to pump the value of; the underlying asset could be, well, Whatever. And Bitcoin itself is open source, so you can copy it and make your very own coin, your very own Whatever. With NFTs, you can make an entire <em>family</em> of “collectible” Whatevers — a strange descriptor given that you can’t actually collect one of each of them, but who really cares if the description makes sense? It doesn’t matter what the art is, or how the technology works, or what the tokens are attached to. It just has to be something you can convince other people to buy. The actual thing can be Whatever.</p>
<p>I think this adequately explains why the proliferation of these guys helped suck all the air out of Twitter. Tens of thousands of grifters lining every sidewalk, each one passionately hawking an indistinguishable Whatever that they don’t actually care about. Endless, <em>endless</em> fake enthusiasm from people all trying to convince each other to buy into their boilerplate box of nothing. Buy <em>my</em> thing! Haha no don’t worry about how much of it <em>I</em> own — let’s talk about how much of it <em>you</em> should own! Hint: it’s a lot!</p>
<p>Kind of a bummer.</p>
<h2 id="the-shape-of-the-web"><a class="toclink" href="proxy.php?url=#the-shape-of-the-web">The shape of the Web</a></h2>
<p>The Web is a cool thing because anyone can just put stuff on it. It is the largest town square bulletin board ever devised. Back in the day, your <span class="caps">ISP</span> would even give you your own website! I don’t think they do that so much any more, but there are more cheap or free options than ever — hell, you can host a little website on GitHub.</p>
<p>And it used to mostly consist of little things made by people, and that was pretty cool! You would see <em>more than four websites in a day</em>. Websites would have <em>colors</em>! They wouldn’t all be designed for a three-inch-wide screen and then just scaled up when you’re at your desk! Twitter once let you set your own background image for when people looked at your profile.</p>
<p>But the trouble with everyone having a bunch of websites is that you lost track of them all and you didn’t really know when they updated and it was hard to talk <em>back to</em> a website. Also, making your own website is kinda hard? You have to, like, learn things.</p>
<p>And so the entire Web sort of congealed around a tiny handful of gigantic platforms that <em>everyone on the fucking planet</em> is on at once. Sometimes there is some sort of partitioning, like Reddit. Sometimes there is not, like Twitter.</p>
<p>That’s… fine, I guess. Things centralize. It happens. You don’t get tubgirl spam raids so much any more, at least.</p>
<p>But the centralization poses a problem. See, the Web is free to <em>look at</em> (by default), but costs money to <em>host</em>. There are free hosts, yes, but those are for static things getting like a thousand visitors a day, not interactive platforms serving <em>a hundred million</em>. That starts to cost a bit. Picture logs being shoveled into a steam engine’s firebox, except it’s bundles of cash being shoveled into… the… uh… website hole.</p>
<p>Traditionally, the way to pay for keeping your website online has been to slather it in ads and suffer the humiliation of Pepsi trying to sell Pepsi halfway down your page. Ads don’t pay very much, but for a moderate-size endeavor, that’s fine. You write your article and put an ad on it and make twenty cents a month or whatever. I don’t know, I don’t run ads because they’re an embarrassing blight that make everything they touch worse.</p>
<p>Together, these forces push big platforms in a very specific direction: <em>maximize how many ads people see</em>. To the exclusion of just about anything else. So Engagement becomes king — it’s okay if your users are <em>miserable</em>, so long as they’re <em>here</em>. It’s okay if the ads are <em>obnoxious</em>, as long as they’re <em>seen</em>.</p>
<p>Then this model spread into phone software. And then into videography. And then, somehow, into fucking, Windows??</p>
<p>And when the primary focus of the business is on the <em>ads</em>, everything else is sort of ancillary — it’s only important insofar as it keeps people around, to look at the ads. It’s jingling keys. It’s… Whatever.</p>
<aside class="aside--fascinating-tangent">
<p>This is why I absolutely cannot fucking stand <em>creative work</em> being referred to as "content". "Content" is how you refer to the stuff on a website when you're designing the layout and don't know what actually goes on the page yet. "Content" is how you refer to the collection of odds and ends in your car's trunk. "Content" is what marketers call <em>the stuff that goes around the ads</em>.</p>
<p>"Content"... is Whatever.</p>
</aside>
<p>This is the driving force behind clickbait, behind thumbnails of white guys making 8O faces, behind red arrows, behind video essayists who just read Wikipedia at you three times a week like clockwork, behind suggestion algorithms, behind recipe blogs that all look the same and have a mile of filler fluff, behind video game websites abandoning the idea of articles and instead turning into <span class="caps">SEO</span> vultures with inexplicably lengthy articles telling you “the blue key is under a rock by the river” so they have more paragraph breaks to put ads between, behind TikTok’s model of being a constant stream which I have to only guess at because I have never had any interest in TikTok but I assume it’s a worse version of YouTube Shorts and I already find <em>those</em> pretty irritating.</p>
<p>It’s all the same thing.</p>
<p><em>Look at it. Look at it, you stupid baby. Look how outlandish or shocking or extreme or dramatic, Whatever it is. Just shut up and look at it, so Home Depot will give me a quarter of a tenth of a cent.</em></p>
<p>At least when I write a lot, you know it’s because I wanted to write it. Also I’m probably not lying to you because someone paid me to do it!</p>
<p>And the only real hope I have here is that someday, maybe, Bitcoin will be a currency, and circulating money around won’t be the exclusive purview of Froot Loops. Christ.</p>
<p>Did you know there were entire get-rich-quick schemes about this? It’s like <a href="proxy.php?url=https://www.youtube.com/watch?v=biYciU1uiUw">writing fake novels</a>. Just make a website with a generic WordPress theme (every website looks the same anyway), write a bunch of bland nothing articles about things that seem a <em>little</em> obscure, and slather it in Google ads. Then let the money roll in from people accidentally finding your website and leaving when they find out it’s useless. But it’s too late because you already got the ad view!</p>
<p>I say “were” because bothering to <em>write</em> generic filler about nothing is passé — now the computer can do it for you!</p>
<h2 id="artificial-reality"><a class="toclink" href="proxy.php?url=#artificial-reality">Artificial reality</a></h2>
<p>If you told me ten years ago that by 2025 we’d have the Star Trek computer, I would’ve been <em>ecstatic</em>. How fucking cool is that! You talk to your computer and it does things!</p>
<p>But we didn’t really get that. We got, I guess, sparkling autocomplete — a fancy chatbot that can string words together in the most inoffensive people-pleasing customer-service voice you’ve ever heard.</p>
<p>The result is something I adamantly do not want to interact with. I do not want to be exposed to <span class="caps">LLM</span> output at any time. It’s <em>noise</em>, and I feel like I get a little dumber every time I accidentally start reading it. My brain is already a bit glitchy, and I really cannot afford to have it work even more less good.</p>
<p>And speaking of things that work even more less good, the technology… sucks? It fundamentally doesn’t do the thing that its investors and diehard fans say it does. It just strings together text that is statistically plausible. And every new alleged advancement comes with some invested airhead billionaire boasting about how the computer is as smart as a Ph.D holder now, and then you see the output and it’s still the most generic banal brain-rotting sludge you’ve ever seen in your life.</p>
<p>Most of my exposure to <span class="caps">LLM</span> output is via Google cramming it everywhere they can think of, and in every instance the result is <em>worse</em>. Google Search keeps redesigning its way around my μBlock filters to dedicate an entire third of my desktop screen height to an “<span class="caps">AI</span> summary” — which either lightly restates the highlighted part of the top search result anyway, or is just total bullshit. YouTube keeps showing a sprinkling of “<span class="caps">AI</span> summaries” under video thumbnails that, without fail, restate the video title in more words. My phone’s fucking <em>weather app</em> has an “<span class="caps">AI</span> summary” with incredible insights like “it’ll get warmer over the course of the week”, which I could readily see for myself if this block of white noise weren’t pushing the <em>temperature graph</em> off the bottom of the screen. Over and over, <em>actual information</em> is moved out of the way to make room for an unreliable lossy compression of that information into text that takes longer to read.</p>
<p>But this is worth billions of dollars.</p>
<aside>
<p>If you are unfortunate enough to have a recent Pixel phone, it's at least possible to reliably turn all this crap off — go to System > Apps > Show all apps, use the three-dots menu to "Show system", tap "AICore", and disable it. There used to be more bits and pieces to turn off, but I guess Android 16 consolidated them? Anyway I've seen nothing resembling LLM output since doing this.</p>
</aside>
<p>I think what really gets me here, and what no one really talks about, is that the bar has been revealed to be so low. <span class="caps">LLM</span> features get bolted onto fucking <em>everything</em> because what they do, what they really do, at their core, is this: <strong>Whatever</strong>. They do Whatever. And that’s great, because Whatever is <em>something</em>. There’s no such thing as an error, no empty results page, no such thing as a missing feature or an uncovered case. Almost without fail, you’ll get <em>something</em>. Is it useful? Is it correct? Is it remotely based in reality? Who cares? Far more important is that there is <em>output</em>. Whatever is apparently better than nothing. Cheap and inoffensive and disposable, like a red beer cup. We are doing to the Internet what we already did to the ocean: filling it with a great swirling vortex of trash.</p>
<h3 id="case-study-1"><a class="toclink" href="proxy.php?url=#case-study-1">Case study 1</a></h3>
<p><span class="dquo">“</span>Ah!” the Hacker News commenters cry. “But have you <em>tried</em> it?” they ask with all the indignity of a kindergartener offended that you won’t eat their mud pie.</p>
<p>But yes, thanks: I was once offered this challenge when faced with a Ren’Py problem, so I grit my teeth and posed my question to some <span class="caps">LLM</span>. It confidently listed several related formatting tags that would solve my problem.</p>
<p>One teeny tiny issue: those tags <strong>did not and had never existed</strong>. I typed this additional context into the computer, and it generated a profuse apology followed by a <em>different</em> set of fictional tags. That was the end of that grand experiment.</p>
<p>The trouble was likely that there <em>was</em> no built-in way to do what I wanted, <em>and</em> no one had ever successfully done it before, so the machine had nothing to draw from… and simply generated something that sounded plausible instead. Because that is what this technology does: it continues a conversation in a way that <em>sounds plausible</em>, as defined by similarity to existing conversations. If there are existing conversations about the topic, great! That makes for a more specific measure of plausibility. If not, <em>even better</em>! Just about <em>anything</em> might be plausible! It can just generate <strong>Whatever</strong>!</p>
<p>I cannot stress enough that this is <strong>worse than useless</strong> to me. Not only did it not answer my question, but it sent me on a wild goose chase making sure I had not somehow overlooked the fake <span class="caps">API</span> it generated.</p>
<p>Like, just to calibrate here: you know how some code editors will automatically fill in a right bracket or quote when you type a left one? You type <code>"</code> and the result is <code>"|"</code>? Yeah, that drives me up the wall. It saves no time whatsoever, and it’s wrong often enough that I <em>waste</em> time having to <em>correct</em> for it.</p>
<p>And that’s a predictable operation that inserts a single character! What we’ve invented is an entire fake persona that will waste your time entire <em>paragraphs</em> at once.</p>
<p>I can’t imagine using this to do any actual work and I don’t understand how anyone else does. This is a whole new kind of failure case we’ve invented. I did also ask <em>people</em> about this problem, and they responded in the ways people might: they said they didn’t know, or they suggested an elaborate and tedious workaround that would technically solve the problem (but introduce new ones). But the <span class="caps">LLM</span> statistically generated something that <em>sounds like an <span class="caps">API</span> that could exist</em>. It produced an answer that was plausible, thorough, informative, relevant, and contained <strong>no useful information whatsoever</strong>. It produced the <em>opposite</em> of information! It produced <strong>noise</strong>.</p>
<p>Why would I want this? Why would I want to use a machine that sometimes generates text that resembles a person confidently lying to me? People are sometimes <em>wrong</em>, sure — that’s why Stack Overflow has downvotes — but this is something else entirely. If a real person did this to you, you would stop asking them questions <em>real</em> fucking fast.</p>
<p><span class="caps">LLM</span> output is crap. It’s just crap. It sucks, and is bad.</p>
<p>Anyway I went on to do the thing I wanted regardless, because I’m a programmer and I know how to make computers do things.</p>
<aside class="aside--fascinating-tangent">
<p>It's not really relevant to the story, but the actual problem I had was that I like to put two spaces between sentences, and I wanted Ren'Py to render this extra space. Unfortunately, Ren'Py collapses all whitespace in strings down to a single space <em>at parse time</em>, which made that seemingly impossible. In fact, a formatting tag could not <em>possibly</em> solve this, because the whitespace collapsing happens to string literals, and formatting happens (much later) to string values.</p>
<p>So I just monkeypatched the parser. Like you do. Ren'Py is wild.</p>
</aside>
<p>I mean, I get it. I was trying to do something that had never been done before. LLMs are fine at things that appear a zillion times in their training data — in fact, this is probably a big part of the trick, because the things that appear more often in their training data are the things people are more likely to ask about <em>in general</em> and thus the things people are more likely to ask an <span class="caps">LLM</span>. But whose creative output consists solely of doing things a million people have already done? Is everyone else working on projects built exclusively out of lists of primes and rebalancing binary trees?</p>
<h3 id="case-study-2"><a class="toclink" href="proxy.php?url=#case-study-2">Case study 2</a></h3>
<p>Back in December, I was <a href="proxy.php?url=https://bsky.app/profile/eev.ee/post/3lclnybocls2n">complaining about something else</a> (surprise, it was Web ads!) and just happened to look at the Visual Studio Code website, most of which was devoted to its <span class="caps">LLM</span> code-completion service, Copilot. I don’t care to desecrate this blog with <span class="caps">LLM</span> output — it’s <a href="proxy.php?url=https://bsky.app/profile/eev.ee/post/3lclp4y5xe22w">on Bluesky if you must</a> — but suffice to say, it wasn’t <em>great</em>. It was a call to a web service, and the generated code failed to encode form data. You know, Computer 101 stuff. Also it was like twice as long as it needed to be. Also it wouldn’t work on <span class="caps">HTTPS</span> websites because the web service’s certificate expired three years ago — which is a fun footgun, since you very well might be on <span class="caps">HTTP</span> localhost, and then it’ll only break when you go live.</p>
<p>I found it highly unlikely that the latest and greatest <span class="caps">API</span> for “get website” couldn’t just encode form data for you, but lo and behold: it can! Copilot just didn’t bother to make use of it. And since Copilot is a Whatever machine and its answers are these one-time disposable things, there’s no mechanism for someone else to come in and go “hey, you forgot to encode the form data”.</p>
<p>What even is this thing we’ve invented? Stack Overflow, but you only get the answers people scramble to type first so they can get the points? Oh and they just lie to you sometimes? Why would I want this?</p>
<p>And I didn’t cherry-pick this example! <em>They chose it!</em> This was the front-page example for a state-of-the-art <span class="caps">LLM</span> integrated with the most popular code editor in the world, all built by one of the richest companies in human history, whose <em>entire business</em> is software and who has specifically invested a zillion dollars in <em>this specific technology</em>. This is the gizmo <strong>at its best</strong>! And it’s crap!</p>
<p>But it does <em>something</em>. And that’s what’s important.</p>
<h3 id="the-broader-culture"><a class="toclink" href="proxy.php?url=#the-broader-culture">The broader culture</a></h3>
<p>There are people who use these, apparently. And it just feels so… depressing. There are people I once respected who, apparently, don’t actually enjoy <em>doing the thing</em>. They would like to describe what they want and receive Whatever — some beige sludge that vaguely resembles it. That isn’t programming, though. That’s management, a fairly different job. I’m not interested in managing. I’m <em>certainly</em> not interested in managing this bizarre polite lying daydream machine. It feels like a vizier who has definitely been spending some time plotting my demise.</p>
<p>It makes programming spaces feel bleaker. I don’t want to help someone who opens with “I don’t know how to do this so I asked ChatGPT and it gave me these 200 lines but it doesn’t work”. I don’t want to know how much code wasn’t actually written by anyone. I don’t want to hear how many of my colleagues think Whatever is equivalent to their own output. I don’t want to keep watching people fall for a carnival trick.</p>
<p>A couple days ago I saw someone (whose bio claimed they’re a Bluesky engineer, but who knows) insist that it’s “very stupid” to <em>not</em> use a chatbot for programming. I just cannot comprehend this. If the task is <em>easy</em>, I could just write the code about as fast as I could describe it anyway. If the task is <em>hard</em>, then it’s all the more likely the generated code will be subtly wrong (or overtly wrong). If it’s something I <em>don’t know</em>, I can go <em>find out</em> about it, and now I know more things. What are you all even writing that so much of it consists of generic slop?</p>
<p>But also… why do you care? Why would someone using a really cool tool that makes them more productive… feel compelled to sneer and get defensive at the mere <em>suggestion</em> that someone else isn’t doing the same? I know there are people who oppose, say, syntax coloring, and I think that’s pretty weird, but I don’t go out of my way to dunk on them. I can’t imagine having a stronger reaction than saying “lmao what” and immediately forgetting about it. I might have strong opinions about what <em>code</em> looks like, because <em>I might have to read it</em>, but why would I — why would <em>anyone</em> — have such an intense reaction to the hypothetical editor setup of a hypothetical stranger?</p>
<p>It feels like the same attitude that happened with Bitcoin, the same smug nose-wrinkling contempt. <em>Bitcoin is the future. It’ll replace the dollar by 2020. You’re gonna be left behind. Enjoy being poor.</em> Sure thing, Disco Stu! There have definitely never been any inventions that turned out to be bad ideas or were just plain forgotten about. But the Bitcoin people <em>make more money</em> if they can shame everyone else into buying more Bitcoin, so of course they’re gonna try to do it. What do programmers get out of this? Unless you work at Microsoft and have a <em>lot</em> of stock options, you aren’t getting rich off of how many people use Copilot.</p>
<p>It’s curiously similar to how, as a fitting segue, Microsoft is now gonna <a href="proxy.php?url=https://www.businessinsider.com/microsoft-internal-memo-using-ai-no-longer-optional-github-copilot-2025-6">factor “<span class="caps">AI</span>” use into employee performance reviews</a>:</p>
<blockquote>
<p><span class="dquo">“</span><span class="caps">AI</span> is now a fundamental part of how we work,” Liuson wrote. “Just like collaboration, data-driven thinking, and effective communication, using <span class="caps">AI</span> is no longer optional — it’s core to every role and every level.”</p>
<p>Liuson told managers that <span class="caps">AI</span> “should be part of your holistic reflections on an individual’s performance and impact.”</p>
</blockquote>
<p>What are we actually saying here — that even Microsoft has to evaluate usage of “<span class="caps">AI</span>” directly, because it doesn’t affect performance enough to have an obvious impact otherwise? That the technology is so limp that even its biggest investor has to <em>strong-arm its own employees</em> into using it? That their own employees don’t <em>want</em> to use it?</p>
<p>Genuinely good new tools don’t tend to need <em>coercion</em> to fuel their adoption only a few years into their existence, right? What the fuck is going on here?</p>
<hr />
<p>Another Bluesky quip I saw earlier today, and the reason I picked up writing this post (which I’d started last week):</p>
<blockquote>
<p>Quitting programming as a career right now because of LLMs would be like quitting carpentry as a career thanks to the invention of the table saw.</p>
</blockquote>
<p>I’m not trying to put the author on blast or anything, so let’s leave it anonymous, but — my guy? My dude?</p>
<p><strong>What on earth are you talking about?</strong></p>
<aside class="aside--note-from-future">
<p>Okay so this was actually intended <a href="proxy.php?url=https://bsky.app/profile/simonwillison.net/post/3lt4ooecpls2i">the opposite way from what I thought</a> because the author has apparently seen it suggested that LLMs are so incredible that programmers should just give up on their careers now (?????), but the "LLMs are just another tool!" thing is <em>absolutely</em> a sentiment I've seen deployed countless times, so let's pretend someone else said this and meant it as "This is just the latest advancement in the trade!" so that I don't have to completely rewrite this part. Ironically, I already rewrote it once to fit with this quote! Anyway sorry Simon oops</p>
</aside>
<p>I don’t know the context for this. What I <em>do</em> know is that a table saw quickly cuts straight lines. That is the thing it does. It doesn’t do Whatever. It doesn’t sometimes cut wavy lines and sometimes glue pieces together instead. It doesn’t roll some dice and guess what shape of cut you are statistically likely to want based on an extensive database of previous cuts. <em>It cuts a straight fucking line.</em></p>
<p>If I <em>were</em> a carpenter, and my colleagues got really into this new thing where you just chuck 2×4s at a spinning whirling mass of blades until a chair comes out the other side… you know, I just might want to switch careers.</p>
<p>I keep seeing this — people compare LLMs to calculators, or screwdrivers, or digital cameras, or whatever. I’m left wondering if the people saying this stuff have ever <em>used</em> any of those things. A calculator does arithmetic for you — thus automating the tedious, repetitive part — but you still have to know <em>which buttons to press</em> to get the answer you want. You can’t just type the entire problem in and get Whatever — something that sounds plausible, with a microscopic disclaimer that checking it for accuracy is <em>your</em> problem.</p>
<p>Calculators do have limitations at their extremes, and if you’re working with extremes, you have to be aware of those. Table saws will (or, used to) cut through fingers just as happily as wood. Tools have edge cases — at their <em>edges</em>. LLMs have edge cases <strong>everywhere</strong>, and they are constantly changing, even minute to minute, even for exactly the same input fed to exactly the same model. It’s also possible to <em>adjust or customize</em> tools in various ways, whereas 90% of the times I’ve seen someone talk about their customized <span class="caps">LLM</span>, all they’ve done is prepend a paragraph like “Please answer as though speaking to a customer.” The state of the art is to ask the computer nicely to do something, add a disclaimer saying it’s not your problem if the computer is racist, and then charge for access.</p>
<p>This is not mere automation. This is a completely new type of thing. We’ve never had a machine that can take almost any input and just do Whatever. But I keep watching people act like it’s the same level of invention as the egg slicer and I feel like I’m losing my fucking mind.</p>
<h3 id="but-what-if-it-gets-better"><a class="toclink" href="proxy.php?url=#but-what-if-it-gets-better">But what if it gets better</a></h3>
<p>I don’t know. What if it does? What does that <em>mean</em>? I hear “better” and I read the press release and in the fine print it says that now it can count the number of letters in “Mississippi” correctly or whatever. And then it’s still crap.</p>
<p>What if it didn’t produce crap? I struggle to imagine such a world, in no small part because the hype around the Whatever machine is so staggeringly overblown. My phone has a dedicated Tensor™ chip to simulate artificial intelligence in the palm of my hand, wow! Here’s what it does: tells me it’ll be hot this week.</p>
<p>But if the machine still just fabricates an elaborate plausible fiction when it doesn’t have an answer on-hand, what good is it? I can always just go find the place it got the answer from originally, and at least then I know that someone <em>wrote it</em>. Someone had a <em>reason</em> to think it, even if they were mistaken. Maybe the well is just permanently poisoned — anytime I see anything I know to be <span class="caps">LLM</span> output, my first assumption is that it’s nonsense, completely divorced from reality.</p>
<p>I know a lot of people have a lot of gripes with LLMs and generative “<span class="caps">AI</span>” that tie them to big grandiose concerns like intellectual property or environmental impact. My gripes are more of a tangled web that I can only summarize as: <em>the vibes are bad</em>. The tone is unbearable. The lying as a fallback is offensive. The advertising keeps focusing on how you can coast through life without caring about your work or family because you can just generate a birthday card or whatever. The people funding and pushing it keep openly salivating at the idea of replacing as much human input as possible with a machine best known for generating titles of books that don’t exist.</p>
<p>I don’t know how you get “better” than this. I don’t know how you make a <em>better</em> Whatever machine.</p>
<h3 id="and-then-theres-the-art-thing"><a class="toclink" href="proxy.php?url=#and-then-theres-the-art-thing">And then there's the art thing</a></h3>
<p>I glimpsed someone on Twitter a few days ago, also scoffing at the idea that anyone would decide <em>not</em> to use the Whatever machine. I can’t remember exactly what they said, but it was something like: “I created a whole album, complete with album art, in 3.5 hours. Why wouldn’t I use the make it easier machine?”</p>
<p>This is kind of darkly fascinating to me, because it gives rise to such an obvious question: if <em>anyone</em> can do that, then <em>why listen to your music</em>? It takes a significant chunk of 3.5 hours just to <em>listen</em> to an album, so how much manual work was even done here? Apparently I can just go generate an endless stream of stuff of the same quality! Why would I want your particular brand of Whatever?</p>
<p>Nobody seems to appreciate that if you can make a computer do something entirely on its own, then that becomes the <em>baseline</em>. </p>
<p>There is a lot that can be said about image generation (little of it polite), but I’m running out of steam a little here. I’d intended to comment on the ongoing efforts to make better and better <em>photo-quality</em> image generation, but I can’t think of much to say beyond: <strong>why the fuck would you work on that?</strong> We don’t have enough trouble with, say, the conservative “news” sphere inventing its own alternate reality that millions of people buy into, simply by <em>lying</em> — now we have to give them a machine tailor-made for creating fake photos and videos too? Why does this need to exist? Why is this <em>in my phone’s fucking camera app</em>? Can’t these people go live on an airgapped island somewhere and work on their new horrifying fraud machine by themselves?</p>
<h3 id="also-i-could-swear-i-saw-google-advertise-that-gemini-can-do-your-homework-for-you"><a class="toclink" href="proxy.php?url=#also-i-could-swear-i-saw-google-advertise-that-gemini-can-do-your-homework-for-you">Also I could swear I saw Google advertise that Gemini can do your homework for you</a></h3>
<p>This is starting to get away from the main thesis of Whatever but every time I hear about students coasting through school just using LLMs, I wonder what we are doing to humanity’s ability to think critically about anything. It already wasn’t <em>great</em>, but now we’re raising a whole generation on a machine that gives them Whatever, and they just take it. You’ve seen anecdotes of people posting comments and submitting papers and whatnot with obvious tells like “As a large language model…” in them. That means they aren’t even reading the words they claim as their own! They just produce Whatever.</p>
<p>Actually hang on this gets me into conclusion territory.</p>
<h2 id="enough-of-whatever"><a class="toclink" href="proxy.php?url=#enough-of-whatever">Enough of Whatever</a></h2>
<p>I remember that Facebook literally proposed running a bunch of its own <span class="caps">LLM</span>-driven fake accounts on its own website. Fake people making fake posts about Whatever, so you’ll have more Whatever to look at, so you’ll see more ads along the way. Monetize the rot, I guess.</p>
<p>I can’t imagine publishing a game with, say, Midjourney-generated art, even if it <em>didn’t</em> have uncanny otherworldly surfaces bleeding into each other. I would find that <em>humiliating</em>. But there are games on the Switch shop that do it. Whatever.</p>
<p>It begins to feel like a broad celebration of mediocrity. <em>Finally</em>, society says, with a huge sigh of relief. <em>I don’t have to write a letter to my granddaughter. I don’t have to write a three-line fetch call. I don’t have to know anything, care about what I’m doing, or even have an opinion.</em></p>
<p><em>I can just substitute some Content™. I can just ask the computer for Whatever</em></p>
<p>But I <em>like</em> programming. I <em>like</em> writing. I like <em>making things</em> and then being able to sit back and look at them and think, holy fuck, <em>I made that</em>. There is no joy for me in typing a vague description into a computer and refreshing my way through a parade of Whatever until something is good enough.</p>
<p>The most obnoxious people like to talk about how Stable Diffusion is “democratizing art” and that is the dumbest thing I’ve ever heard. There is no fucking King of Art decreeing who is allowed to draw and who isn’t. You could do it. You could do it right now. But it’s hard, so you’d rather spend that time crying on Twitter about how unfair it is that <em>learning a skill takes work</em> and thank god the computer can give you all of the admiration with none of the effort now.</p>
<p>This is an incredibly weird moment. There have always been inventions that make some craft easier (but sometimes a little more shoddy as well). There have always been people who resented the idea that the thing they work very hard at is now more accessible. America’s Protestant work culture is deeply entangled with this as well, but I don’t value sweat in and of itself — I have a broader objection.</p>
<p>Because this is something else. What’s being sold to us is a machine that is promised to do <em>everything</em>. That’s far beyond a tiny question like “should you know how to manually focus in order to take a photograph” — it gets at the notion of <em>thinking about, or doing,</em> <strong><em>anything at all</em></strong>.</p>
<p>I don’t think anyone is obligated to do anything in particular. If you don’t want to draw, or write, or compose, or program, or whatever, then don’t! That’s fine.</p>
<p>But I think the core of what pisses me off is that selling this magic machine <strong>requires</strong> selling the idea that <em>doing things is worthless</em>. Because if <em>doing something</em> has some value, then it must be somehow <em>better</em> than pushing a button and receiving Whatever for essentially no cost. If you’re some assclown like Sam Altman, whose graph-go-up depends on convincing you to replace all your employees with ChatGPT, you <em>have to destroy that idea</em>. It is the greatest threat to your business model. You have to destroy the idea that <em>things are worth doing</em>.</p>
<p>I think that sucks, I think he sucks, and I think his machine sucks. So fuck him and fuck his machine.</p>
<p><strong>Do things. Make things.</strong> And then put them on your website so I can see them.</p>Anise lives2025-06-10T02:00:00-07:002025-06-10T02:00:00-07:00Eeveetag:eev.ee,2025-06-10:/blog/2025/06/10/anise-lives/<div class="prose-full-illustration">
<img alt="close-up photo of a black sphynx cat face, staring directly and intently at the viewer from mere inches away; behind him are computer monitors and other desk clutter, but mostly obscured by cat face and blurred by the focal distance" src="proxy.php?url=https://eev.ee/media/2025-06-anise-lives/anise-wants-prince-food.jpg"/>
</div>
<p><em>(a post rescued from Cohost, originally Aug 2024)</em></p>
<p>Some backstory may be necessary.</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/2025-06-anise-lives/anise-wants-prince-food.jpg" alt="close-up photo of a black sphynx cat face, staring directly and intently at the viewer from mere inches away; behind him are computer monitors and other desk clutter, but mostly obscured by cat face and blurred by the focal distance">
</div>
<p><em>(a post rescued from Cohost, originally Aug 2024)</em></p>
<p>Some backstory may be necessary.</p>
<h2 id="some-backstory"><a class="toclink" href="proxy.php?url=#some-backstory">Some backstory</a></h2>
<p><a href="proxy.php?url=https://en.wikipedia.org/wiki/Feline_infectious_peritonitis"><span class="caps">FIP</span></a> is a cruel joke of a cat disease. It’s a mutation of the feline coronavirus, which as you might imagine is fairly common, so virtually any cat might develop it at any time. It tends to afflict very young or very old cats. If left untreated, it is virtually always fatal in a matter of weeks. And there is no cure.</p>
<p>At least, that was the state of things when <a href="proxy.php?url=https://eev.ee/blog/2013/04/30/goodbye-styx/">my first cat, Styx, died of <span class="caps">FIP</span></a>.</p>
<p>That was a long time ago now and it was very sad. It’s still very sad, just, less frequently. Sometimes I try to give his death meaning by reassuring myself that it led me to meet Anise, but that’s trading one companion for another, which is kind of weird? I think nowadays I find it more comforting to think about how the universe is chaotic and things <em>just happen</em> and there doesn’t need to be any… moral weight on them.</p>
<p>Anyway.</p>
<h2 id="the-regular-story"><a class="toclink" href="proxy.php?url=#the-regular-story">The regular story</a></h2>
<p>Anise is my second cat and he has always been a sturdy guy. He’s never had any real health problems.</p>
<aside class="aside--fascinating-tangent">
<p>The closest was some five or six years ago. I took him to the vet for... something... and the vet told me he either had a UTI, <em>or</em> some chronic condition. And it was probably the chronic condition. Because Anise was 5 now. Which is middle-aged. He's middle-aged now. He's not so young any more. This vet was adamant about driving this point home, and I have no idea why — it's not like I strolled in boasting about my cat's youth and how my entire self-image revolved around it.</p>
<p>Anyway it turned out he just had a UTI, and after like a week of antibiotics he was fine. So I have no idea what all that was about.</p>
</aside>
<p>But then a couple years ago, maybe around the end of 2022, I noticed he was losing weight. And this freaked me out <em>a bit</em> because Styx’s first symptom was weight loss that only I had noticed — I’d gone out of town for a weekend and returned to find his back looking kinda bony. And now I realized Anise was looking kinda bony.</p>
<p>And then he started getting lethargic. He’d spend basically all day, every day, just sleeping. And yeah, I know, cats sleep a lot, but they don’t <em>just</em> sleep. Anise would get out of his cat bed, drink some water, maybe eat a single kibble, and then return to his cat bed. That was all I saw from him all day. And he’s usually very, um… sociable? Demanding? Sociamanding.</p>
<p>So we went to the vet (a different one, now), and they poked and prodded him a bit, and said he was basically fine. Great news.</p>
<p>But he kept slowly… <em>shrinking</em>. And he didn’t seem to enjoy eating. I thought about this some and wondered if maybe he had a <em>dental</em> problem, that chewing on kibble was painful. I took him back and asked them to look at his teeth real good, and lo and behold, he had a couple bad teeth! So we farted around for over a month with appointments and some other vet thing I can’t remember before he could actually get those teeth pulled, and then he had a month of meds afterwards, and in the meantime I’m doing stuff like putting kibble in a coffee grinder and turning it into a paste so he can just lick it up, because at least he seems to eat a bit more when I do that.</p>
<p>And <em>then</em> I wait for the post-op soreness to probably go away, which is mostly just waiting for some length of time that seems reasonable because cats aren’t real big on telling you where or how much it hurts, and meanwhile he’s now having diarrhea that seems to be especially unpleasant for him. So it’s back to the vet, and they still don’t see any clear problems, but maybe we could try this bland sensitive stomach food and mix in some pumpkin? And I go off and buy a bunch of prescription food and mix in a truly staggering amount of pumpkin purée — the advice is to give a cat up to a teaspoon a day, and I need to give him a tablespoon or more to keep him moderately regular — but it does seem to help, and he seems to enjoy eating it more. Progress! Progress?</p>
<p>We came to call this concotion Prince Food. Anise soon learned that the only source of Prince Food was <em>me</em>, and every four hours like clockwork, he would ask for some Prince Food. He did this by sitting on my desk, facing me, putting him at very nearly my eye line, and just staring at me. That’s what the photo is: Anise asking for Prince Food. I literally put my phone as close to my face as I could and took a picture. That is what my field of vision looked like, four to six times a day. For months. Absolutely ridiculous.</p>
<p>This is what I mean about Anise being sociamanding — he is very aware of the things he likes, and most of the ways he’s found to ask for them are almost cartoonishly obnoxious. Going a whole day without being physically bugged for something is <em>rare</em>.</p>
<p>…But he was still slowly losing weight. We went back and forth to the vet, trying various things, waiting weeks each time to see if anything would change — sphynxes especially seem to need time to adjust to a new diet, so it was really hard to know when to call something a definitive failure — and it wouldn’t. It was uniquely exhausting. I really didn’t like the endless treadmill of just trying endless minor variations of food and medication, because each one meant risking he’d be even more miserable for a while until we gave up on it. But even the super duper bland prescription food with a ton of pumpkin mixed in was just barely keeping him digesting anything, and he wasn’t gaining any weight back, so it didn’t seem like a permanent solution. And time continued to pass.</p>
<p>I think at the most extreme, I found out he’d dropped from 11 pounds to something like 7½. An entire <em>third</em> of my cat had disappeared. And the response from the vet felt like "Huh! That’s weird." Meanwhile I was losing my fucking mind in this slow burn that dragged on for months.</p>
<p>They eventually directed me to an internist — something I wish had occurred to me much, <em>much</em> sooner — who gave him an ultrasound. It showed his small intestine was… flattened? Like, you know how your intestines are full of cilia, all these tiny fleshy fingers, to increase surface area for absorbing nutrients? His were just like, flat. So his guts were straight up, uh, <em>not working good</em>. Well no fucking wonder. But we still didn’t know why.</p>
<p>We went through this cycle of trying a couple meds again — I remember steroids, because autoimmune was on the table — and <em>still</em> nothing changed. We were approaching a <em>year</em> of this at this point, I was constantly thinking about what a massive percentage of Anise’s lifespan that is, and I was about two seconds away from just blowing up the universe, when the internist said:</p>
<p><span class="dquo">“</span>Maybe it’s <span class="caps">FIP</span>?”</p>
<p>And I said:</p>
<p><span class="dquo">“</span>what”</p>
<p>Because it couldn’t be. He didn’t have any of the bloodwork of a cat with <span class="caps">FIP</span>. The usual <span class="caps">FIP</span> age range is very young or fairly old, and he was close to smack-dab in the middle of those. He didn’t have the swollen belly that most cats get with <span class="caps">FIP</span>. (Apparently that happens when there’s no immune response at all.) And most conspicuously, he was <em>still alive</em>, after this extensive period of fucking around.</p>
<p>But there is no <em>conclusive</em> test, so it’s hard to definitively say yes or no. The internist tested his poop for coronavirus antibodies and they were off the charts — like, I think, literally beyond what the test can accurately count — so she said, ok, let’s just assume it is.</p>
<p>And I said, I know exactly what to do.</p>
<h2 id="the-cure"><a class="toclink" href="proxy.php?url=#the-cure">The cure</a></h2>
<p>Because you see, in the decade since Styx died, someone <em>has</em> found a cure for <span class="caps">FIP</span>. A cure that is effective as often as <span class="caps">FIP</span> itself is fatal, some ridiculous percentage like 97% of the time. It is just unbelievably good. The turnaround from “almost always die” to “almost always live” gives me fucking whiplash. It even works on cats who are in worse shape than Styx was when I euthanized him. Truly a shining moment of human ingenuity.</p>
<p>…There are some problems.</p>
<p>One is that the company that invented it has refused to submit it for <span class="caps">FDA</span> approval. I don’t really know why. I vaguely remember a suggestion that they wanted to market it as a treatment for <em>human</em> coronaviruses and thought submitting it for veterinary use would interfere with that? I don’t know. That sounds absurd to me. I definitely gave my cat several drugs I recognized as things we also give to humans in the course of this adventure, so why would this be a problem?</p>
<p>It doesn’t even have a real name. It’s just <a href="proxy.php?url=https://en.wikipedia.org/wiki/GS-441524"><span class="caps">GS</span>-441524</a>. If you want some, you have to get it off-brand from a slightly dubious seller in China. I don’t know why it’s China but all the sellers seem to be in China.</p>
<p>The other problem is that you can’t really give it orally, because at this point the cat’s guts <em>can’t absorb anything</em>. So you have to give it subdermally.</p>
<p>Like, with a syringe.</p>
<p>To a cat.</p>
<p>Who has no idea what you’re doing.</p>
<p>And just as a bonus, it has to be suspended in an acidic solution, so it <em>really</em> stings.</p>
<p>And your vet can’t help you do it.</p>
<p>She <em>did</em>, however, prescribe us a big pile of gabapentin, a fairly safe anticonvulsant with the convenient side effect of significant drowsiness.</p>
<p>So once a day for over a month, Ash would give Anise a capsule of gabapentin, transforming him into Stumble Anise. Then I would load a syringe, we would hold him together, and Ash would inject him, and he would be real mad, and I would give him a little cat stew treat thing, and he would go in his toasty box.</p>
<p>(Anise started to get pretty cold, since we live in Colorado, it was winter, he doesn’t have fur, and he’d lost most of his body fat. There’s a furnace vent near my desk, so I got one of those Costco display boxes and set it up with a blanket inside overlapping the vent a bit, so the warm air would flow into it and keep it warmed up. Anise spent a <em>lot</em> of time in there that winter and would go there to sulk after we inexplicably bit him every day.)</p>
<p>And then a miracle occurred, and he started gaining weight.</p>
<p>After a month or so it’s apparently safe to switch from injections to tablets, so we did that, and he kept regaining weight. And he stopped having diarrhea.</p>
<p>And then he stopped asking for Prince Food. He would just go eat kibble and it would be fine. We might even still have a can or two of bland prescription food somewhere that I never ended up needing.</p>
<p>And now he’s just fine. He’s not bony any more. He’s active and engaging and incredibly annoying again. He doesn’t have such immense stomach pain that he wakes up growling.</p>
<p>It feels like a fucking miracle. He caught the same deadly disease that killed my first cat, and he fought it so hard no one even suspected he had it because he wasn’t <em>sickly enough</em>, and then with just a bit of black market medicine he shrugged it off like it was nothing.</p>
<p>I think that’s pretty cool of him.</p>
<h2 id="the-aftermath"><a class="toclink" href="proxy.php?url=#the-aftermath">The aftermath</a></h2>
<p>This was all so stressful like you would not even believe, and the whole saga spanned over a year. It was often hard to work. Or sleep. At best, feeding him Prince Food every four hours was still an <span class="caps">ADHD</span> nightmare — I could only feed him on my desk lest other cats interfere, he would take some twenty minutes to eat (he’s always been a dawdling eater), and I couldn’t really do anything else with him in the way. I’m very glad it’s over, and I’m sure he is too.</p>
<p>All this has left me thinking about the series of <a href="proxy.php?url=https://eev.ee/blog/tags/eulogy/">cat eulogies</a> I’ve written here and how I’m kind of tired of doing that. So I think I would like to post all about Anise while he’s, y’know, still alive. I’ll get on that.</p>
<hr />
<p>And thank you, Ash, for doing some of the hard parts. I really struggle with wrangling a cat who <em>really</em> doesn’t want to do something — I would’ve had a hard time getting him to swallow a pill, let alone holding him still enough to stick a needle in him. I do not know how I would’ve done this without you. So as far as I’m concerned, you saved his life. Thank you.</p>🔞 vignettes2025-04-21T18:33:00-07:002025-04-21T18:33:00-07:00Eeveetag:eev.ee,2025-04-21:/blog/2025/04/21/vignettes/<p><div class="prose-full-illustration"><img alt="" src="proxy.php?url=/media/updates/vignettes.png"/></div></p>
<p><em>vignettes</em> is the spicy visual novel we’ve been plugging away at for the past year or so. It’s about transformation and sex and conflict and magic tricks. I think it’s pretty good! But I’m biased, so you’ll have to draw your own conclusions. By… playing it…?</p>
<p>It’s currently <a href="proxy.php?url=https://eevee.itch.io/vignettes">ten bucks on itch</a>, <strong>but</strong>: we’ll be adding more stories over time, and slightly bumping the price every time. So this is probably the cheapest it’ll ever be. How compelling!</p>
<p>Some thoughts follow, as per usual.</p>
<p><div class="prose-full-illustration"><img src="proxy.php?url=/media/updates/vignettes.png" alt=""></div></p>
<p><em>vignettes</em> is the spicy visual novel we’ve been plugging away at for the past year or so. It’s about transformation and sex and conflict and magic tricks. I think it’s pretty good! But I’m biased, so you’ll have to draw your own conclusions. By… playing it…?</p>
<p>It’s currently <a href="proxy.php?url=https://eevee.itch.io/vignettes">ten bucks on itch</a>, <strong>but</strong>: we’ll be adding more stories over time, and slightly bumping the price every time. So this is probably the cheapest it’ll ever be. How compelling!</p>
<p>Some thoughts follow, as per usual.</p>
<hr />
<p>I’ve been trying to <em>finish</em> another adult <span class="caps">VN</span> for a while. We did <a href="proxy.php?url=https://eevee.itch.io/cherry-kisses">Cherry Kisses</a>, which even did alright <a href="proxy.php?url=https://store.steampowered.com/app/1259530/Cherry_Kisses/">on Steam</a>… but that was 2019.</p>
<p>Before that was <a href="proxy.php?url=https://floraverse.itch.io/alices-day-off-demo">Alice’s Day Off</a>, a “demo” which never became a full thing because it relied on a combinatoric explosion and it turns out that might be a bad idea even if you know it’s a bad idea and think you can turn it into a good idea. (I will definitely try to make it work again one day.)</p>
<p>And then I don’t know what happened exactly. A couple years passed as a sort of indistinct haze. I wonder if anything happened in 2020 to cause that.</p>
<p>But by 2022 we got back to it and tried something more story-heavy this time: <a href="proxy.php?url=https://eevee.itch.io/clover-and-over-prologue">Clover and Over… “prologue”</a>, which remains only a prologue. There was a branching story planned to go with it but we just… didn’t… do it.</p>
<p>I don’t even know <em>why</em> we didn’t do either of these things. We just ran out of steam, I guess. The thing itself was too big and time-consuming and it was just <em>draining</em> to keep working on something without feeling like we were getting meaningfully closer to an end point.</p>
<p>I’ve been struggling for a few years, really. Even fox flux has been blocked on level design in a way I don’t seem capable of resolving. I don’t know why I’m working on anything or who I’m working on it for. Redoing this website is one of the larger things I’ve done in ages and it still took way longer than it should have. It’s like I have a leak, and something is draining out of me faster than I can refill it, but I don’t know where it is or how to plug it.</p>
<p>well anyway</p>
<p>I did come up with a workaround here, at least — <em>vignettes</em> is really a framing device for multiple stories, meaning we can release something now <em>and also</em> build on it later. We have a loose arc in mind that’ll span half a dozen or so stories, but even if we vanish off the face of the Earth, what’s already there is still a… complete thought.</p>
<p>And that’s nice, I think.</p>
<p>I hope the next few parts don’t take so long to get out. This took us over a year — partly because of other things going on, partly because I feel like an empty husk. But I have a big pile of characters I originally designed for <em>Clover and Over</em> and haven’t really gotten to share with the world yet, so I’d like to do that.</p>
<p>I dunno. Yeah. I wanted to also say stuff about how this format lets me skip doing a bunch of Ren’Py setup work every time and makes it easier to play with the <span class="caps">VN</span> format without dedicating a whole entire big thing to it, but then this took a bit of a weird turn, sorry. Hope you enjoy the game.</p>Eevee gained 4,219 experience points…2025-01-17T20:00:00-08:002025-01-17T20:00:00-08:00Eeveetag:eev.ee,2025-01-17:/blog/2025/01/17/eevee-gained-4219-experience-points/<p>Eevee grew to level 38!</p>
<p>It’s been a few years since I did one of these. My birthday in 2022 was kind of overshadowed by the <a href="proxy.php?url=https://eev.ee/blog/2022/01/25/goodbye-pearl/">loss of our darling cat Pearl</a>. But I think I’d like to get back into it, to christen the redone website.</p>
<p>Eevee grew to level 38!</p>
<p>It’s been a few years since I did one of these. My birthday in 2022 was kind of overshadowed by the <a href="proxy.php?url=https://eev.ee/blog/2022/01/25/goodbye-pearl/">loss of our darling cat Pearl</a>. But I think I’d like to get back into it, to christen the redone website.</p>
<p><audio src="proxy.php?url=https://eev.ee/media/2012-01/levelup.ogv" controls autoplay style="margin: 1em auto;"></p>
<h2 id="this-year"><a class="toclink" href="proxy.php?url=#this-year">This year</a></h2>
<h3 id="untitled-trefoil-game"><a class="toclink" href="proxy.php?url=#untitled-trefoil-game">Untitled Trefoil game</a></h3>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/blog/2025/01/17/eevee-gained-4219-experience-points/media/2025-01-birthday/trefoil-demo.gif" alt="Clip of a pixel art game where three chibi characters walk around a grassy field, encounter a sheep called a "Cubaa", and attack it until it's "not feeling it any more"">
</div>
<p>This was my entry for my annual horny game jam, <a href="proxy.php?url=https://itch.io/jam/strawberry-jam-8">🔞 Strawberry Jam 8</a>. Tragically, we did not make it. I think I bit off a bit more than I could chew by trying to design and also implement an <span class="caps">RPG</span> battle system in an engine I haven’t used much.</p>
<p>The engine in question is <a href="proxy.php?url=https://godotengine.org/">Godot</a>, which I keep <em>desperately</em> trying to get a foothold in, and then… not… doing. By “foothold” I mostly mean a little bundle of common code I’ve written atop Godot for dealing with cases that come up a lot in games but that are too specific for the engine to include directly — a basic actor type, scene switching, that sort of thing. Just a mini library for me that’s already made all the decisions I would make, so I’m not starting completely from scratch.</p>
<p>I guess the other half of a “foothold” is figuring out how to make the engine do things <em>at all</em>. Other than a <a href="proxy.php?url=https://eev.ee/blog/2017/10/13/coaxing-2d-platforming-out-of-unity/">brief foray into Unity many years ago</a> that didn’t go much further than twiddling with player physics, I just haven’t really tried using an integrated engine, and <em>boy</em> — in some ways, it’s harder than rolling your own everything. If a built-in gizmo doesn’t do something I take for granted that I should be able to do, then… fuck me, I guess?</p>
<p>But I would still like to get away from actually having to build every feature from scratch myself (like I do in <a href="proxy.php?url=https://love2d.org/">LÖVE</a>) because it just takes so much dang time, which I am then not spending on <em>making a video game</em>. So I would really like to get something resembling a game built and released in Godot! And this is the latest of several attempts in which I do not do that.</p>
<p>I think there were two big stumbling blocks here that were not conducive to wrangling a new-to-me engine in a jam setting, and I think both are partly effects of Godot’s being new-in-general:</p>
<ul>
<li>
<p>There’s often not an obvious good way to approach a problem with Godot’s primitives. Nodes are a cool idea and all, but I think it’s less than obvious that you might want to use node names as an ad-hoc interface, e.g. by giving actors a collection of behavior nodes named after components or states or something.</p>
</li>
<li>
<p>There’s a lot of little oddball behavior and little feature gaps, most notably from GDScript, Godot’s bespoke language that’s “we have Python at home”. But for a non-code example: there’s no way to give a <span class="caps">UI</span> widget a maximum size, so if a long word sneaks into your text somewhere… don’t do that, I guess? Stuff like that that will cost you a few valuable hours chasing down only to find that it doesn’t exist.</p>
</li>
</ul>
<p>I took notes at the time about a lot of the speed bumps I ran into, and many of them were about GDScript specifically, but I never wrote them up because… I mean, christ, I don’t want to do another epic teardown <span class="caps">XD</span> of a language. It’s embarrassing enough being known for the first one. Instead, let me try to summarize with a single bullet point that I hope will convey <em>the vibe</em> to anyone who’s ever thought about language design for a few minutes:</p>
<ul>
<li>Some builtin functions can accept an arbitrary number of arguments. There is no way to write a function in user code that accepts an arbitrary number of arguments.</li>
</ul>
<p>Anyway the short version here is that I picked too big of a project to do in a month <em>and</em> I tried to do it in an engine that still has a lot of rough edges.</p>
<h3 id="anise"><a class="toclink" href="proxy.php?url=#anise">Anise</a></h3>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/blog/2025/01/17/eevee-gained-4219-experience-points/media/2025-01-birthday/anise2020.jpg" alt="Photo of a black sphynx cat in a wintery sweater looking somewhat solemnly at the camera, with an American flag background edited in around him and "anise 2020" poorly scribbled underneath">
</div>
<p>Anise was sick for much of 2023, which kind of fucked up that whole year, but we finally cured him this past spring. Hooray! Here is a photo of him from his 2020 Presidential campaign.</p>
<p>This is worth its own post, which I have already written, but it was on Cohost, which is now gone. Maybe I’ll port it over and add a bunch of cat photos to it.</p>
<p>Anyway he’s fine and that’s great.</p>
<h3 id="vignettes"><a class="toclink" href="proxy.php?url=#vignettes">vignettes</a></h3>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/blog/2025/01/17/eevee-gained-4219-experience-points/media/2025-01-birthday/vignettes-walk.png" alt="Screenshot of a visual novel where two sheep-like characters are out for a walk; one, named Clover, is saying "It's not too far, I think. About half a mile past that burrito place where none of the waiters wear pants."">
</div>
<p>So, we released <a href="proxy.php?url=https://eevee.itch.io/cherry-kisses">🔞 Cherry Kisses</a> a few years ago. (Five. <em>Five</em> years ago?? Oh my god) It’s a little spruced-up jam game where you walk around and talk to customers and do little sex scenes with them, accompanied by art, and also there’s an overarching puzzle aspect you can completely ignore if you want.</p>
<p>This was <em>originally</em>, as I said, a jam game, which we didn’t actually finish in time, but which took maybe a month and a half to do. At the time, I thought: wow, great! This porn stuff is E-Z. We should just crank a couple of these out a year, in between other stuff we’re doing!</p>
<p><em>Five years later</em>, we have not yet released another porn game. Or another game on Steam at all! Not for lack of trying — we started several (overscoped) visual novels that never ended up finding their footing.</p>
<p><span class="caps">COVID</span> and other world events kind of put a damper on things, creating a broader problem: it’s just been <em>hard</em> to drum up the right mood for writing extended lighthearted sex romps.</p>
<p>That said, we <em>finally</em> have something that is <em>almost</em> done. Actually it’s been asymptotically approaching “done” for a while. It’s tentatively called <strong>vignettes</strong>, and the idea is that we will release a <em>shorter</em> story, then go back and update the game later with more shorter stories, and also play around with the format if I feel like it too. Hopefully this will fix some of our scoping problems.</p>
<p>It’s still not done. But we did most of the work for it this past year. Like 90%. It’s so close. I’m getting back to it after I finish this post and another urgent thing.</p>
<p>I’m actually going slowly insane over this, because I’ve been designing characters and whatnot for <span class="caps">VN</span> purposes for <em>years</em>, and they have all been rolling around in my head like marbles that whole time, and <em>no one outside this house knows anything about them</em>. I need to let them out!</p>
<h3 id="fox-flux"><a class="toclink" href="proxy.php?url=#fox-flux">fox flux</a></h3>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/blog/2025/01/17/eevee-gained-4219-experience-points/media/2025-01-birthday/fox-flux-home.png" alt="Screenshot of a pixel art platformer, showing the protagonist (an orange fox) standing in a sort of basement of an abstract home, with jelly beans and chick-shaped candies floating next to her and gift boxes and furniture on a level above">
</div>
<p>Wow! When I sat down to write this post I thought I’d done basically nothing on <a href="proxy.php?url=https://eevee.itch.io/fox-flux-deluxe">fox flux</a> all year. But I guess I did a lot actually. It’s not moving quite as quickly as I’d hoped, and the game is in a bit of disarray so there haven’t been <a href="proxy.php?url=https://www.patreon.com/eevee">Patreon</a> builds in a bit, but, it <em>is</em> moving.</p>
<p>I don’t want to write out a whole gritty changelog here, but suffice to say I implemented a <em>bunch</em> of stuff that had been languishing as little stubs for a long time, so the game feels a lot closer to feature-complete. Now I just have to make a zillion levels! How hard could <em>that</em> be?</p>
<p>(It’s very hard. It’s where I got blocked, creatively! All my level ideas turned out not fun and I didn’t have any more so I went to work on other stuff for a while. Puzzle level design is fuckin’ <em>gnarly</em> my dudes)</p>
<h3 id="lexys-labyrinth"><a class="toclink" href="proxy.php?url=#lexys-labyrinth">Lexy's Labyrinth</a></h3>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/blog/2025/01/17/eevee-gained-4219-experience-points/media/2025-01-birthday/lexys-labyrinth.png" alt="Screenshot of Lexy's Labyrinth, a tile-based browser puzzle game, showing a small fox player in a maze of ice, water, hot coals, and land mines, as well as a lot of surrounding UI">
</div>
<p><a href="proxy.php?url=https://c.eev.ee/lexys-labyrinth/">Lexy’s Labyrinth</a>, my free Chip’s Challenge emulator, was like 90% finished, so I sat down and 100% finished it. Or, I dunno, 99%.</p>
<p>Highlights include:</p>
<ul>
<li>
<p>Now has <a href="proxy.php?url=https://c.eev.ee/lexys-labyrinth/#pack=cclp5"><span class="caps">CCLP5</span></a>, the latest and greatest community level pack! Adding this in is what got me back to working on <span class="caps">LL</span>, so, thanks for putting it together, everyone.</p>
</li>
<li>
<p>The tileset got a lot of touching up, and it now sports a brighter palette, instead of merely a copy of an old dull (not even pastel, just, dull) fox flux palette. The website is pink to match Lexy, too, though I fear it might be too reddish?</p>
</li>
<li>
<p>You can hold <kbd>R</kbd> to restart the level! At last!</p>
</li>
<li>
<p>Undo now uses much less memory, and the undo buffer is limited by size rather than time (though it will always save at least 30 seconds). On the most pathological built-in level I could find, 30 seconds was about 12 <span class="caps">MB</span>, and the limit is 10 <span class="caps">MB</span>, so this should be a huge improvement pretty much anywhere. On a sokoban-like level where the player is mostly stopping and thinking while nothing else happens, undo is virtually unlimited.</p>
</li>
<li>
<p>Rewind now accelerates the further back you go, too.</p>
</li>
<li>
<p>There are several touchscreen control schemes now: swipe, tap relative to the player, or tap relative to the viewport. So you can try whichever is least bad. There’s also partial gamepad support, though only within a level.</p>
</li>
<li>
<p>A bit more <span class="caps">CC2</span> behavior is now shown visually within the level where it wouldn’t be in <span class="caps">CC2</span>, like dynamite always showing its full explosion radius.</p>
</li>
<li>
<p>Compatibility is vastly improved, <em>and</em> more of the built-in levels are beatable. (Some of the built-in levels are designed for <span class="caps">CC1</span>, but the default rules are <span class="caps">CC2</span>-like and <em>slightly</em> different. A couple levels now have manual patches specifically to make them beatable, a tactic borrowed from ZDoom.)</p>
</li>
<li>
<p>Support for Lynx mode has gone from “very bad” to “pretty solid”! It’s still not speedrun legal, largely because it doesn’t fully emulate frankly insane bugs like actors being able to teleport on top of each other, but it should be sufficiently accurate for normal purposes.</p>
</li>
</ul>
<p>The editor is also vastly, <em>vastly</em> better; it has multiple new tools for otherwise awkward tasks, it supports arbitrary selections (including a new wand select tool), it supports <span class="caps">CC1</span>-style tile connections, and it can export levels in <span class="caps">CC1</span> format!</p>
<p>I’d had code for that last thing written for ages and just never plugged it in, and I didn’t even notice until I saw someone in the Bit Busters Discord comment that <span class="caps">LL</span> wasn’t useful for <span class="caps">CC1</span> level editing because it couldn’t actually export as <span class="caps">CC1</span>. Whoops.</p>
<p>There are also lots of experimental extra tiles, though they aren’t all fully implemented, and I haven’t made any “official” levels with them. I <em>did</em> start on my own level pack for Lexy’s Labyrinth specifically, but I completely forgot about it until I was writing this post just now. Wonder if I’ll ever finish that. 200 levels is a lot, but it’s also good practice.</p>
<h3 id="doom-stuff"><a class="toclink" href="proxy.php?url=#doom-stuff">Doom stuff</a></h3>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/blog/2025/01/17/eevee-gained-4219-experience-points/media/2025-01-birthday/idfkn.png" alt="Screenshot of an "idgames archive" website listing, showing two Doom WADs that list their maps with some stats, whether they support skill levels, whether they have music, etc.">
</div>
<p>In August, Microsoft unveiled an entirely redone official Doom release, now with Boom support, making it compatible with more of the Doom ecosystem.</p>
<p>That was weird, because Boom is a third-party fork of the open source Doom release, meaning it’s <span class="caps">GPL</span>. And there was no release of the current Doom codebase.</p>
<p>Turns out that what Microsoft did was pay someone to cleanroom Boom from scratch, effectively laundering the <span class="caps">GPL</span> off of it, so that they could add open source extensions to Doom (an open source game) and make it proprietary again.</p>
<p>I found this… <em>frustrating</em>. But a lot of Doom people didn’t really care and mostly found it neat that they can play Doom on an XBox now. (It’s notoriously difficult to put someone else’s <span class="caps">GPL</span> code on a console. The console APIs are all covered under NDAs — you know, to prevent anyone from finding out that the <strong>X</strong>Box uses Direct<strong>X</strong> — so you’d be in a position where the <span class="caps">GPL</span> requires you to release your modified code, but the <span class="caps">NDA</span> requires you to not do that.)</p>
<p>Instead of sitting around being mad forever that the very people who’ve benefitted so much from Doom’s being open source don’t really care about open source, I tried to pour it into something slightly more constructive, and so I started tinkering with an improved <a href="proxy.php?url=https://www.doomworld.com/idgames/">idgames</a> frontend. I guess the idea was that people mostly seemed to value having a <span class="caps">WAD</span> browser built into Microsoft Doom, but there’s no reason we can’t have that for the entire archive of everything ever made, right?</p>
<p>I ran out of steam before it got <em>too</em> far, but I did get it doing a few interesting things, like automatically producing screenshots of the opening shot of each level (which you can see in the screenshot above). I had some other ideas, like trying to infer qualitative descriptions of level size and difficulty, but didn’t quite get around to it. Oh well.</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/blog/2025/01/17/eevee-gained-4219-experience-points/media/2025-01-birthday/doom-text-generator.png" alt="Screenshot of my Doom text generator, with a selection of fonts and colors and text rendering sliders, currently displaying "Instead, I worked on this some more"">
</div>
<p>So I went back to my <a href="proxy.php?url=https://c.eev.ee/doom-text-generator/">Doom text generator</a>, a former advent calendar project that I cranked out in a day and <a href="proxy.php?url=https://eev.ee/blog/2019/12/01/doom-text-generator/">wrote about before</a>. I’d seen a couple people mention having actually used it, which was cool, so I went and did some stuff to it.</p>
<ul>
<li>
<p>It could only render its own built-in fonts, so an obvious extension was to extend it to load fonts of any format from a provided <span class="caps">WAD</span> or <span class="caps">PK3</span>, all client-side.</p>
</li>
<li>
<p>It has a truckload more built-in fonts now, courtesy of <a href="proxy.php?url=https://forum.zdoom.org/viewtopic.php?t=33409">Jimmy Paddock’s collection</a>. So the Doom text generator can now generate Duke Nukem text, too. Weird.</p>
</li>
<li>
<p>It can, finally, combine multiple fonts in a single message, using a tiny bbcode-like markup language.</p>
</li>
<li>
<p>It can generate a bunch of images in bulk, which is exceptionally handy for level authors targeting vanilla-like Doom, where you have to provide your own level name images. It can even read the level names directly from a <code>MAPINFO</code> file that more advanced ports would use to render names themselves.</p>
</li>
</ul>
<p>Now it’s got a <a href="proxy.php?url=https://www.doomworld.com/forum/topic/147196-doom-text-generator-%E2%80%94-last-updated-2024-11-25/">Doomworld thread</a> and <a href="proxy.php?url=https://github.com/eevee/doom-text-generator">source code</a> (little redundant since it’s all shipped to the client) and everything. It’s like a real project! I’m glad people find it useful.</p>
<p>It even got a Cacowards 2024 sidebar shoutout, which is cool, making this basically the only thing of note I’ve ever done in the Doom ecosystem. The Boom license laundering also got an explicit shoutout, though, so. Cool.</p>
<h3 id="sudoku"><a class="toclink" href="proxy.php?url=#sudoku">Sudoku</a></h3>
<div class="prose-full-illustration">
<img style="max-height: 20em;" src="proxy.php?url=https://c.eev.ee/puzzles/sudoku-20241018-1.svg" alt="A sudoku with no given digits, but several diagonal clues outside the grid and some killer cages inside">
</div>
<p>I made some sudoku, after realizing I could just make some sudoku if I wanted to. My first one is appropriately titled <a href="proxy.php?url=https://sudokupad.app/cb5yikms2b">1</a> (killer + little killer), and the rest are on my new <a href="proxy.php?url=https://c.eev.ee/puzzles/">puzzle index</a>. Speaking of which—</p>
<h3 id="you-are-here"><a class="toclink" href="proxy.php?url=#you-are-here">You are here</a></h3>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/blog/2025/01/17/eevee-gained-4219-experience-points/media/2025-01-birthday/you-are-here.png" alt="Screenshot of this post">
</div>
<p>I <a href="proxy.php?url=https://eev.ee/blog/2024/12/03/fresh-start/">redesigned the website</a> in the wake of Cohost’s shutdown, and after several years of not writing much. Time will tell if it encourages me to write more going forward, but so far, so good.</p>
<p>I also wrote those pages about stuff! The list of <a href="proxy.php?url=https://eev.ee/fyi/variant-sudoku/">variant sudoku types</a> (which I’ve even used for my own reference already), and ports of my <a href="proxy.php?url=https://eev.ee/toys/lights-out/">Lights Out</a> and <a href="proxy.php?url=https://eev.ee/toys/rush-hour/">Rush Hour</a> <span class="caps">CSS</span> crimes from Cohost.</p>
<h2 id="this-year_1"><a class="toclink" href="proxy.php?url=#this-year_1">This year</a></h2>
<p>I basically forget about any aspirational list like this within a week, but I would <em>really really</em> like to:</p>
<ul>
<li>
<p>Get <strong>vignettes</strong> released, <em>and</em> get a couple more stories added to it!</p>
</li>
<li>
<p>Get <strong>fox flux</strong> out of its level design rut and just accelerate into building the game proper. Also more patron builds.</p>
</li>
<li>
<p>Write more, I think. I do kind of miss it.</p>
</li>
</ul>
<p>I’d like to… reconnect with the world, I guess. Everything feels disconnected. I dropped Twitter and tried to rebuild on Cohost, did not really succeed at that (there’s a post in that, too), and then Cohost went down. Now there’s Bluesky, I guess, but I feel like I’m two platforms and several years in the hole. <em>And</em> a couple closer friendships disintegrated over the same period, so I have been adrift as all hell.</p>
<p>And the primary way I know how to connect with anyone is through my work! So I need to make some!</p>Fresh start2024-12-03T15:41:00-08:002024-12-03T15:41:00-08:00Eeveetag:eev.ee,2024-12-03:/blog/2024/12/03/fresh-start/<p>I hit a point where I just didn’t like this website any more. It was too… <em>stuffy</em>. Posts kept getting longer, more elaborate, more time-consuming to write. I didn’t recognize the tone any more, and when I look back at older posts, those are way snarkier than they need to be. I think I was trying to be taken seriously-but-not-too-seriously, and the voice that developed as a result was just really weird.</p>
<p>I kept looking at this blog and thinking… who <em>is</em> this? Who wrote this? And who is supposed to write more of this?</p>
<p>So I’ve changed <em>everything</em>.</p>
<p>I hit a point where I just didn’t like this website any more. It was too… <em>stuffy</em>. Posts kept getting longer, more elaborate, more time-consuming to write. I didn’t recognize the tone any more, and when I look back at older posts, those are way snarkier than they need to be. I think I was trying to be taken seriously-but-not-too-seriously, and the voice that developed as a result was just really weird.</p>
<p>I kept looking at this blog and thinking… who <em>is</em> this? Who wrote this? And who is supposed to write more of this?</p>
<p>So I’ve changed <em>everything</em>.</p>
<h2 id="design"><a class="toclink" href="proxy.php?url=#design">Design</a></h2>
<p>The <a href="proxy.php?url=https://c.eev.ee/eev.ee-2024/blog.html">previous design</a> was based around “keep looking like sort of like the default Octopress theme I used to have”. I don’t think that was a great design philosophy, not least of all because I stopped using Octopress in 2012, and Octopress itself has been abandoned since 2015. But it was also just… beige. I think I was hoping that brown-ish would be reminiscent of Eevee colors?</p>
<p>So I redid it from scratch, based on the <em>new</em> design philosophy of “I like it”. And you know what? I like it! I hope you like it too. But also, I don’t care too much if you don’t, because it’s for me!</p>
<p>Also, hey, did you know <span class="caps">CSS</span> can do all kinds of crazy stuff now? Like nesting blocks? This blog used to use <span class="caps">SCSS</span>! Now it doesn’t need to! And I barely had to change anything! Maybe there’s a post in there.</p>
<p>You might need a recent browser, but you should be using a recent browser anyway.</p>
<h2 id="landing-page"><a class="toclink" href="proxy.php?url=#landing-page">Landing page</a></h2>
<p>The <a href="proxy.php?url=https://eev.ee/">landing page</a> is also completely different. (I’ve preserved <a href="proxy.php?url=https://c.eev.ee/eev.ee-2024/">the previous one</a>, if you’re interested.)</p>
<p>My time on Cohost, which allowed near-arbitrary inline <span class="caps">HTML</span> and <span class="caps">CSS</span>, has rekindled a joy in doing stupid tricks with <span class="caps">CSS</span>, and so I’ve glued together a mountain of stupid tricks to make something more playful and distinct. Also I can draw better now than I could a decade ago, so I flexed those muscles a bit, too.</p>
<p>One thing I do slightly lament is that my games used to be above the fold (at least on my screen), and now they’re not. But I think this design actually rewards… scrolling down? So hopefully that helps. Not like I’m collecting metrics or anything.</p>
<h2 id="resume"><a class="toclink" href="proxy.php?url=#resume">Résumé</a></h2>
<p>I’ve intended to do this for ages. The old landing page contained an exhaustive list of… <em>most</em> things I’ve made or worked on, which made it kind of a cluttered mess. Now that exhaustive list has a real home as <a href="proxy.php?url=https://c.eev.ee/">the landing page for c.eev.ee</a>. I’ve been using this to host stuff (like <a href="proxy.php?url=https://c.eev.ee/lexys-labyrinth/">Lexy’s Labyrinth</a>) for ages, but the root page has been a 403 that whole time. Now it’s not! Wow!</p>
<p>Also: <a href="proxy.php?url=https://c.eev.ee/puzzles/">a list of my puzzles</a>! There aren’t too many yet, but maybe there will be??</p>
<h2 id="pages"><a class="toclink" href="proxy.php?url=#pages">Pages</a></h2>
<p>Pelican has both “articles”, which are dated, and “pages”, which are not. I looked over a lot of my old posts in the course of this redesign, and a lot of them are either out of date w.r.t. technical information, about an event that was only interesting for a brief time, or just… unmaintained?</p>
<p>Which all makes sense for something that has a <em>date</em>, right? Like, Wikipedia articles don’t have a date. Those are assumed to be reasonably current. And there’s something a little sad about writing a very lengthy post with a lot of details about something, and then watching it sink into the ocean of time. But keeping a post with “2015” in the <span class="caps">URL</span> up-to-date indefinitely doesn’t seem quite right.</p>
<p>So I’m taking a crack at “pages”. I expect the presentation will change a bit as I accumulate more, but I’ve seeded the idea with a couple starter pages:</p>
<ul>
<li>
<p>Two pure-<span class="caps">CSS</span> toys/puzzles that were originally Cohost <span class="caps">CSS</span> crimes, now tidied up and with full
explanations: <a href="proxy.php?url=https://eev.ee/toys/lights-out/">Lights Out</a> and <a href="proxy.php?url=https://eev.ee/toys/rush-hour/">Rush Hour</a>.</p>
</li>
<li>
<p>A lengthy list of <a href="proxy.php?url=https://eev.ee/fyi/variant-sudoku/">variant sudoku types</a> with full rules and examples, something that I hadn’t seen anywhere else.</p>
</li>
</ul>
<h2 id="i-changed-all-the-urls-sorry"><a class="toclink" href="proxy.php?url=#i-changed-all-the-urls-sorry">I changed all the URLs sorry</a></h2>
<p>At some point I put the category in the <span class="caps">URL</span>, so “normal” blog posts were at <code>/blog/foo</code>, whereas my <a href="proxy.php?url=https://eev.ee/blog/dev/">dev log posts</a> were at <code>/dev/foo</code>. I don’t know why I thought that was a good idea, and it makes it annoying to reshuffle the categories, so I’ve collapsed everything back into <code>/blog</code>.</p>
<p>There are redirects up the wazoo so this shouldn’t matter to anyone. But if you find a link to my site that 404s, I must’ve missed a redirect, so, please let me know.</p>
<p>The cool news is that instead of reverse-chrono categories, there’s now a much better way to find a post you’re looking for: I made a <a href="proxy.php?url=https://eev.ee/site-index/">full list of all my writing</a>! Enjoy. But don’t read anything from before 2015.</p>
<h2 id="pardon-our-dust"><a class="toclink" href="proxy.php?url=#pardon-our-dust">Pardon our dust</a></h2>
<p>I probably forgot a lot of little things, and the layout is still a bit work-in-progress on very small screens. I’m also not entirely sure how to convey the distinction between articles/pages at a glance, and I don’t have a real name for the c.eev.ee page, so it’s called something different everywhere it appears.</p>
<p>Feel free to send me your nitpicks; I’d like this design to be more actively maintained than the old one was.</p>
<p>Coming up next: Posts?? More posts?????? Remember when I wrote posts????????</p>Monday Night Itch #1: Mystery Trap Adventure2022-01-31T21:15:00-08:002022-01-31T21:15:00-08:00Eeveetag:eev.ee,2022-01-31:/blog/2022/01/31/monday-night-itch-1-mystery-trap-adventure/<p><strong>Welcome</strong> to Monday Night Itch, a harebrained scheme to encourage folks to play more non-<span class="caps">AAA</span> games by adding a touch of social gamification. I thought I would be tweeting my adventures here, but I just had an experience so profound it can only be captured within a blog post.</p>
<p><strong>Welcome</strong> to Monday Night Itch, a harebrained scheme to encourage folks to play more non-<span class="caps">AAA</span> games by adding a touch of social gamification. I thought I would be tweeting my adventures here, but I just had an experience so profound it can only be captured within a blog post.</p>
<h2 id="the-rules"><a class="toclink" href="proxy.php?url=#the-rules">The rules</a></h2>
<p><span class="dquo">“</span>Rules” is a strong word, but nevertheless:</p>
<ul>
<li>
<p>Every Monday, find a game on <a href="proxy.php?url=https://itch.io/">itch.io</a>, and pay at least $2 for it.</p>
<p>You can buy a game with a price tag, or download a free game and leave a tip, but the point of this endeavor is to put money into more places in the ecosystem. (Note that it <em>is</em> possible, though uncommon, for a developer to disable payments altogether.)</p>
</li>
<li>
<p>Play it.</p>
</li>
<li>
<p>Leave a nice comment.</p>
</li>
<li>
<p>Tell at least one person what you played, and what you thought about it.</p>
</li>
</ul>
<p>That’s it. Buy a game, play it, tell someone about it. You can stream it, tweet it, screenshot it, or just tell your boyfriend about it. You don’t have to like it</p>
<p>Your score is how many times you’ve done this, and your streak is how many weeks you’ve done it in a row.</p>
<h2 id="some-other-quick-tips-about-itch"><a class="toclink" href="proxy.php?url=#some-other-quick-tips-about-itch">Some other quick tips about itch</a></h2>
<p>The <a href="proxy.php?url=https://itch.io/app">itch app</a> is cool. It’s a pretty thin wrapper around the website, but it adds automatic updating and big red “Launch” buttons and other stuff to make it feel a bit more like a Steam-ish thing. Do keep in mind that devs can upload whatever they want, and sometimes the itch app gets confused.</p>
<p>If you’re not a fan of running mystery software you downloaded from the Internet, you can just play web games and leave tips on those.</p>
<p>There are <em>a lot</em> of <span class="caps">NSFW</span> games on itch, but they’re hidden from the main browse pages by default. You can enable them site-wide in your <a href="proxy.php?url=https://itch.io/user/settings">user settings</a>, or add <code>/nsfw</code> to the end of a browse page <span class="caps">URL</span> (for example, <code>https://itch.io/games</code> → <code>https://itch.io/games/nsfw</code>) to force a list of <em>only</em> <span class="caps">NSFW</span> games.</p>
<h2 id="the-main-event"><a class="toclink" href="proxy.php?url=#the-main-event">The main event</a></h2>
<p>I decided I wanted to reward Linux releases, and also chip a few bucks towards games with a price tag that aren’t necessarily getting much exposure, so I went to the full list of <a href="proxy.php?url=https://itch.io/games/newest/platform-linux/store">recent paid Linux games</a>. This is how I discovered <a href="proxy.php?url=https://rvedastudios.itch.io/mystery-trap-adventure">Mystery Trap Adventure</a>.</p>
<p>I found myself <em>very</em> much wanting to play this, but I also found myself wondering what sort of impact I should be trying for as the very first iteration of this project. Would I torpedo it if I played a game made by a less experienced dev? Are people looking to this expecting me to uncover unknown indie gems, like I’m wandering a beach with a metal detector?</p>
<p>I checked the dev’s itch profile and this is their <em>ninth</em> project. Every single previous work of their has only a single comment: from them, announcing that comments can be left below. That’s heartbreaking to me, and what made me absolutely sure I wanted to play this. I want to make their day.</p>
<p>And then, dear reader, I felt ashamed. Because who the fuck cares. The world already has enough people who believe that indie games are only valuable if they create the illusion of an eight-digit budget, and I am not here to enable them. Creative work does not need to be polished, mass-appeal, least common denominator stuff handed down from heaven by a billion-dollar international corporation in order to be interesting or worthwhile.</p>
<p>But more importantly, it’s my thing and I’m gonna do whatever the hell I want.</p>
<p><div class="prose-full-illustration"><img src="proxy.php?url=/media/monday-night-itch/001-mystery-trap-adventure/title.jpg" alt="The title screen for Mystery Trap Adventure: a collage of mismatched artwork on a nearly cyan background"></div></p>
<p>And so, Mystery Trap Adventure.</p>
<p>The first thing to note is that the game does not, in fact, have a Linux release. I did strongly suspect this, since a single download is flagged as all of Windows, Mac, and Linux, but the only way to be sure was to buy it. (They’re asking $4; I paid them $10.) Even Wine had trouble with it, for some reason, so I had to play it on our Windows media center.</p>
<p>It’s a sidescrolling platformer where you play as a dragon; you can jump about one tile high (roughly your own height) and shoot fireballs (useful for destroying bricks and defeating the boss). The main obstacle is spikes, which kill you instantly.</p>
<p>Right at the beginning, there’s a block you have to jump on top of, and it was very obvious that I sort of “stuck” to the side of it if I touched it. I thought at first that this was the result of a common platforming gotcha: if you model the player as a dynamic body and implement movement (including air control) as a force on them, then they will stick to walls as long as the corresponding direction is held. This happens because forces on dynamic bodies are external, as though a giant ghost hand were pushing them — so if a player is trying to air control into a wall, the <em>friction against the wall</em> will hold them in place, just as if you were holding a book against a wall with your hand.</p>
<p>(Solving that problem is beyond the scope of this post, sorry.)</p>
<p>Okay, common pitfall, no big deal. I wander ahead a bit. I encounter a slice of watermelon, which allows me to teleport a short distance <em>once</em>. I screw this up the first time while messing with the controls — there’s a wall directly in front of it, so the teleport must be used to skip past that wall — and have to restart.</p>
<p>Now something interesting happens. I’m in a pit with walls on both sides. I can’t teleport again, and even if I could, there are spikes beyond the next wall, so that would kill me immediately.</p>
<p><div class="prose-full-illustration"><img src="proxy.php?url=/media/monday-night-itch/001-mystery-trap-adventure/pit.jpg" alt="A screenshot of the situation just described"></div></p>
<p>It dawns on me that this microscopic game has <em>walljumping</em>.</p>
<p>I’m still fairly certain that the player character is a dynamic body, but now I wonder: is the wall stickiness actually due to the friction interaction, or is it a deliberate feature to enable walljumping?</p>
<p>Or, perhaps more likely, is it <em>both</em>? Did the developer trip over this pitfall, and decide to make a gameplay feature out of it? It almost seems unbelievable. I wouldn’t consider walljumping a <em>basic</em> platforming ability, and it’s not obvious how to solve the friction problem, but it seems that this relatively new developer may have solved both problems by simply smashing them together.</p>
<p>And if that’s the case, dearest reader: I <em>fucking love it</em>. That is the true spirit of game development, I think — you have a big complicated simulation you want to make, and you have a big complicated engine that you want to make do it, and you have to kinda mold both of them into fitting better with the other.</p>
<p>I don’t know. I could be completely wrong about this came to be. Or they could have copy/pasted from someone else who had this idea. Either way, it made me smile to see.</p>
<p>The walljumping controls are, ahem, not exactly intuitive, which is why it took me nonzero time to realize it was an ability at all. But honestly, I liked that too. Nowadays, everyone knows exactly how every platforming ability is “supposed” to work, because devs are all copying the same ideas from each other that have been refined over a thousand different iterations. This reminded me of playing games in the early and mid 90s, before everything had standardized as much, when part of the game itself was just working out the right muscle memory to make the right things happen. It’s surprising to find nostalgia in a game because it’s <em>not</em> like others I’ve played before, but there it was. Working out the right timing without any visual cues felt like a puzzle in itself, and getting out of the pit without landing in the spikes was remarkably satisfying. (If it helps: I used different hands for movement and jumping, and I landed on top of the right wall before trying to jump over the spikes.)</p>
<p>Beyond this, the tone changes somewhat to <span class="caps">IWBTG</span>-esque traps with no telegraphing. Walking directly to the right will cause spikes to appear from the ground, killing you instantly. Thankfully there aren’t too many of these, and the game is very short, so simply memorizing the handful of places they appear is easy enough.</p>
<p>I have less to say about the rest of the game; you get another quirky powerup you only use once, dodge another couple surprise traps, and face a single boss. The boss is a very large human warrior dude who walks straight at you and swings his sword, which kills you. There’s another fruit above you, but it seems out of reach. He is definitely too tall to jump over. The only solution I found is to simply spam fireballs at him before he can reach you, but I don’t know if this is intended. It seems like it can’t be, since his “health bar” takes the form of a grid of his face behind him, and from where you enter the area, you can’t actually see the whole grid? So surely I’m supposed to be able to get further to the right? But I don’t know.</p>
<hr />
<p>I finished the game and came back to the following reply to my original thread about this whole concept:</p>
<blockquote>
<p>most, i.e. all, small Indy games are terrible.</p>
</blockquote>
<p>What a snotty, entitled, mean-spirited sentiment. As if the very existence of a game with lower production values than Resident Evil 8 were a personal offense. It seems to be fairly common, too, and I just do not understand it. Small indie games aren’t trying to squeeze you for more money, lure you in with gambling, exploit your friendships, make your entire life revolve around them. They’re just <em>there</em>.</p>
<p>This attitude is like showing up to everyone who mentions YouTube just to proclaim that everything on it sucks, because Paramount movies are better. That’s great, no one asked! Sometimes I just want to see a seven-second clip of a kitten filmed in a dark room by a $20 phone, because dammit, kittens are still fun to watch. No one makes a point of dunking on videos like that, so I don’t know why anyone is so harsh on amateur games either. <em>Especially</em> when making games is so much more difficult!</p>
<p>Mystery Trap Adventure is that video. Someone had an idea, worked out how to express it, and put it out into the world just because they wanted to. I don’t expect anyone else to buy it or play it; I just want you to know that <em>I</em> did, and it made me smile for a few minutes.</p>Goodbye, Pearl2022-01-25T22:57:00-08:002022-01-25T22:57:00-08:00Eeveetag:eev.ee,2022-01-25:/blog/2022/01/25/goodbye-pearl/<p><div class="prose-full-illustration"><img alt="Pearl laying on carpet, bathed in a sunbeam that highlights her peach fuzz" src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-radiant.jpg"/></div></p>
<p>A Chronicling of the Lyfe and Times of one Miss Pearl Twig Woods, who has Passed at a Young Age from Troubles of the Heart. She is survived by Anise, her Arch Nemesis; Cheeseball, her Adoptive Ruffian; and Napoleon, her Star-Crossed Suitor for Whom she Longed from Afar.</p>
<p><div class="prose-full-illustration"><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-radiant.jpg" alt="Pearl laying on carpet, bathed in a sunbeam that highlights her peach fuzz"></div></p>
<p>A Chronicling of the Lyfe and Times of one Miss Pearl Twig Woods, who has Passed at a Young Age from Troubles of the Heart. She is survived by Anise, her Arch Nemesis; Cheeseball, her Adoptive Ruffian; and Napoleon, her Star-Crossed Suitor for Whom she Longed from Afar.</p>
<p><div class="gallery">
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-sit-hat.jpg" class="photo" title="Hello. This hat is for Pearl now thank you. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-sit-hat_m.jpg" alt="Pearl sits on a hat on the dining room table, regarding the camera with a slightly tilted head"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-cool-arm.jpg" class="photo" title="What a cool arm. That's how you know this is a cat with an attitude. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-cool-arm_m.jpg" alt="Pearl settled across Ash's knees and arms, with one paw tucked in a way that looks like crossed arms"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-happy.jpg" class="photo" title="Delight. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-happy_m.jpg" alt="Pearl looks very happy"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-depth-of-field.jpg" class="photo" title=""><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-depth-of-field_m.jpg" alt=""></a>
</div></p>
<p>Pearl is… difficult to describe. She had such a strong, vibrant personality.</p>
<p>She was <em>lovely</em>, that’s for certain. She loved everyone she met. And while various people — friends, vets, etc. — have met our cats and always <em>liked</em> them, I don’t believe anyone has met Pearl and not <em>adored</em> her. Anise will check out your stuff and perhaps jump on you; Cheeseball will do antics for you and rub on your leg; but Pearl would accept you into her life and be very directly, personally affectionate with you specifically. She made you feel special.</p>
<p>At the same time, she was very fussy, very particular, and had a very strong sense of… her place in the world, I suppose. If she liked something, she would be having it. If she didn’t like something, she would make that exceptionally clear. She was never <em>mean</em>, but she would be very vocal about her boundaries.</p>
<p>It wasn’t uncommon to wake up to Pearl repeatedly headbutting me right in the face, pressing her head up under my chin, or giving me a nuzzle with the entire length of her body, purring all the while. If she was happy to see you, she made an entire production out of it. It wasn’t just us; guests who slept on the couch also got the Pearl wake-up call.</p>
<p>It was <em>also</em> not uncommon to wake up because Pearl had decided that she needed my pillow, and somehow this very small cat took up the entire thing. I couldn’t <em>move</em> her; trying to displace her from a comfortable spot would generally earn you a sad, offended meow, after which you felt guilty for even having entertained the notion in the first place.</p>
<p>One of her particular quirks was to often “bury” her food when she was done with it, or at least paw fruitlessly at nearby carpet. On its own, this is endearing but not unusual — burying leftovers is a common cat instinct, even if we’ve not seen it in our other cats. What made it a uniquely Pearl trait was that she would <em>also</em> perform this ritual if offered something she didn’t want at all. I laughed every time; it was such an audacious way to indicate utter disinterest. Take it away, please. Put it in a hole, if you would.</p>
<p>She got, more or less, everything she wanted. If she claimed a spot, everything about her expression and body language indicated it was clearly hers, even if that spot was your body. (Naturally, if you moved too much or even sneezed suddenly, she would tell you off for that too.) If she wanted to ride on your shoulders, that’s what would be happening. If it was time to feed her and she was too comfortable in her cat tree, well, we’d just have to hold the food up for her. She had a way of looking very pleased with herself that was impossible to argue with.</p>
<p><div class="gallery">
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-pillow-theft.jpg" class="photo" title="A not-uncommon configuration. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-pillow-theft_m.jpg" alt="Pearl rests comfortably on a pillow, while Ash is asleep below it, having conceded it entirely to a small cat"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-beanbag.jpg" class="photo" title="I guess this belongs to Pearl now. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-beanbag_m.jpg" alt="Pearl sits right in the center of a beanbag that is vastly bigger than her, leaving no real space for anyone else"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-claws-1.jpg" class="photo" title="Pearl could be very frumpy about getting her claws trimmed. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-claws-1_m.jpg" alt="Pearl is held up in Ash's lap, making a very frumpy face, while Ash trims her claws"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-claws-2.jpg" class="photo" title="Mmmmmnnnn... I dunno. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-claws-2_m.jpg" alt="Pearl, a moment later, tries to lean her way out of Ash's lap"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-shoulder-ash.jpg" class="photo" title="Pearl lives here now thank you. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-shoulder-ash_m.jpg" alt="Pearl washes her face while laying across Ash's shoulders"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-pampered.jpg" class="photo" title="Pearl the pampered princess. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-pampered_m.jpg" alt="Ash sits on a couch with a sketchbook in their lap and Pearl, wrapped in a blanket, under one arm, getting her chin rubbed"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-nuzzle-blur.jpg" class="photo" title="This is tricky to get a photo of, but it's also just the Pearl experience. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-nuzzle-blur_m.jpg" alt="Pearl steps down over my shoulder, rubbing her whole body against my face as she goes, displacing my glasses"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-nuzzle-nose.jpg" class="photo" title="It's like being punched, over and over, by a very tiny and lovely fist. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-nuzzle-nose_m.jpg" alt="Pearl headbutts my nose so hard it bends to one side"></a>
</div></p>
<p>I first met Pearl in 2014, shortly after we moved to Las Vegas. She was <em>tiny</em>, even for a kitten, and apparently the runt of her litter. I don’t remember what specifically compelled Ash to adopt another cat, except that they love cats, but what a selection.</p>
<p>I cannot stress enough how small she was. You know those solid wood desks that have a column of drawers built into them on one side? You know how they often have a little decorative shape carved out at the bottom with molded edges? Pearl could crawl into that space. I couldn’t believe it the first time I saw it; the gap is so short that I’d never even thought to categorize it as a <em>space</em>, let alone one a cat might enter, but she slunk into it like it was nothing. I was so worried we’d have to move the desk somehow to get her out, but she usually turned around and came right out again. I still remember the very last time she did it — I could tell she was having to shimmy a bit to fit in there, and she must have noticed too, because I never saw her even try it again.</p>
<p>The other cats had somewhat mixed feelings. Napoleon didn’t like her at all and hissed directly in her face, but… after that, I don’t remember any bad reactions from him at all, so I guess he warmed up quick. Anise did not seem to understand what a kitten was, tried to play with her, and then acted very confused when that didn’t seem to work. And Twigs…</p>
<p>Oh, Twigs. Twigs was <em>jealous</em>. He had always been Ash’s cat, he had <em>made himself</em> Ash’s cat, and he very quickly inferred that Pearl was a threat to his position. Another cat! In Ash’s lap! Unthinkable!</p>
<p>On one particular night Ash had barred Twigs from the bedroom to sleep with just Pearl, but came downstairs to visit the kitchen. Twigs ran up to them, looked them dead in the eye, and let out a huge sad wail to convey his feelings about the depths of this betrayal.</p>
<p>They let him into the bedroom after that, but he opted to sit across the room and stare daggers at Pearl, moving a little closer every half hour until he was on the far corner of the bed. Just staring.</p>
<p>Ash eventually had to bribe him by putting some cottage cheese on Pearl’s head, after which he decided Pearl was okay. Also he found out that he could fit himself in Ash’s lap alongside Pearl, so that probably helped.</p>
<p><div class="gallery">
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-kitten-meeting-napoleon-1.jpg" class="photo" title="Pearl meets Napoleon, our largest cat, for the first time. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-kitten-meeting-napoleon-1_m.jpg" alt="Pearl, a very small sphynx kitten, sniffing the nose of a furred cat twice her height"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-kitten-meeting-napoleon-2.jpg" class="photo" title="Napoleon is not impressed. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-kitten-meeting-napoleon-2_m.jpg" alt="Same pair as previous photo, but Napoleon is hissing and Pearl is flinching"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-kitten-anise-1.jpg" class="photo" title="Pearl is so small she has to climb the stairs one at a time. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-kitten-anise-1_m.jpg" alt="Tiny kitten climbing up a single step of a flight of stairs, while our black sphynx Anise sits and watches"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-kitten-anise-2.jpg" class="photo" title="Anise is not sure what to do with this. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-kitten-anise-2_m.jpg" alt="Anise straddles a couple higher steps and leans down to sniff Pearl"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-kitten-anise-3.jpg" class="photo" title="Maybe if he taps it, something will happen. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-kitten-anise-3_m.jpg" alt="Anise lightly paps Pearl's head with one paw"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-kitten-anise-4.jpg" class="photo" title="Very suspicious. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-kitten-anise-4_m.jpg" alt="Anise recoils when Pearl looks at him"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-kitten-anise-5.jpg" class="photo" title="Tactical retreat? I love this photo, I have no idea what he was doing. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-kitten-anise-5_m.jpg" alt="Pearl yawns while Anise peeks around a corner at her from the top of the stairs"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-kitten-twigs-bribe-1.jpg" class="photo" title="Twigs regards Pearl with deep suspicion. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-kitten-twigs-bribe-1_m.jpg" alt="Pearl lays sleepily on Ash while Twigs sits on their desk and looks at her with a somewhat sour expression"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-kitten-twigs-bribe-2.jpg" class="photo" title="Twigs decides Pearl can't be that bad if she tastes like cheese. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-kitten-twigs-bribe-2_m.jpg" alt="Twigs licks some cottage cheese off of Pearl's head"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-kitten-stretched.jpg" class="photo" title="Pearl loves to be comfortable. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-kitten-stretched_m.jpg" alt="Pearl lays outstretched alongside Ash's forearm, barely longer or wider than it"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-kitten-slipper.jpg" class="photo" title="She was so tiny that she could just sit in one of my slippers, like it was her personal cat bed. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-kitten-slipper_m.jpg" alt="A very small Pearl settles on a slipper, which is very slightly bigger than her"></a>
</div></p>
<p>Oh, and she loved to be cozy. She <em>loved</em> to be cozy. Sphynxes are naturally drawn to warmth, of course, but Pearl elevated it to an artform. If I’m propped up in bed, Anise might stand next to me to look at the covers expectantly, or he might just lay down nearby. Pearl would stand right on top of me and <em>pull</em> at the covers with impressive force until I lifted them for her, let her lay on my chest, and tucked her in.</p>
<p>We’d often find Pearl very awkwardly tucked under the edge of a blanket somewhere, having attempted to insert herself beneath it with mixed success. We described this as Pearl doing it all by herself, and complimented her on how talented she was, and then fixed the blanket for her.</p>
<p><div class="gallery">
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-doing-it-2.jpg" class="photo" title="She did it all by herself! "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-doing-it-2_m.jpg" alt="Pearl lays tucked within a blanket, curled around her like a nest"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-doing-it-3.jpg" class="photo" title="She's SO talented. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-doing-it-3_m.jpg" alt="Pearl is underneath a blanket but her entire back half is sticking out"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-doing-it-4.jpg" class="photo" title="She tried her very best. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-doing-it-4_m.jpg" alt="Pearl lays beneath a single folded-over corner of an electric blanket, which barely covers her at all"></a>
</div></p>
<p>We have heater vents in the floor, and one of her favorite pastimes was to sit on one of those, often covering the entire thing, and be gently toasted from below. Sometimes Anise would see what a great idea that was and try to share it and they would end up squabbling.</p>
<p>If there was a sunbeam to be found, Pearl would find it. Much like with vents, she didn’t like to share sunbeams, even if they were half the width of the room. She found it first, you see.</p>
<p>Other places she discovered that were lovely and toasty included: in front of the fridge where the warm air vented out from the bottom; straddling the <span class="caps">PS4</span> so the fan blew onto her tummy; next to or underneath my laptop; on top of my computer case which has a fan vent on top; in front of the heat dish we got while our furnace wasn’t working; and in a laundry hamper full of freshly-dried laundry.</p>
<p>She liked to go outside, too, during the summer. All of our cats are indoor-only, but once in a while we’ll take the more well-behaved ones (not Cheeseball) into the backyard to wander around on the porch and sniff things and enjoy the sun and look at a bird.</p>
<p><div class="gallery">
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-vent.jpg" class="photo" title="One of Pearl's favorite places. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-vent_m.jpg" alt="Pearl sits atop a heater vent in the floor"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-sunbeam.jpg" class="photo" title="This is a pretty good summary of Pearl. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-sunbeam_m.jpg" alt="Pearl sits in a small square sunbeam, roughly the size of her silhouette, in the middle of the floor"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-fridge-vent.jpg" class="photo" title="Pearl no it's so dusty down there "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-fridge-vent_m.jpg" alt="Pearl sits, paws folded, next to the vent at the bottom of the fridge"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-heat-dish.jpg" class="photo" title="Honestly kind of amazed she never got a tan from this. Also pictured: string, her favorite. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-heat-dish_m.jpg" alt="Pearl sits right in front of a heat dish"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-computer-vent.jpg" class="photo" title="Pearl please I am trying to compile GZDoom. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-computer-vent_m.jpg" alt="Pearl sits on top of my computer case with her front half mostly overlapping a fan vent"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-ps4.jpg" class="photo" title="Warm comes out of back there. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-ps4_m.jpg" alt="Pearl straddles a PS4, one back foot atop it while the rest of her is wedged behind it where the fan is"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-laundry.jpg" class="photo" title="I probably wasn't going to fold this anytime soon anyway. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-laundry_m.jpg" alt="Pearl's head pokes out from within a basketful of laundry"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-sunbeam-table.jpg" class="photo" title="Sometimes I wonder how she even finds these. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-sunbeam-table_m.jpg" alt="Pearl sits on a table looking deeply content as the sun shines directly on her"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-watched-outside.jpg" class="photo" title="See if you can find Pearl in this photo. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-watched-outside_m.jpg" alt="Three cats look out through a sliding glass door; outside, in the distance, Pearl lies on sunny concrete"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-backyard-exploring.jpg" class="photo" title="Pearl checks out some wood chips. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-backyard-exploring_m.jpg" alt="Pearl stands alongside the back fence of the yard, while Ash checks their phone nearby"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-backyard-posed.jpg" class="photo" title="Pearl reflects on her next move. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-backyard-posed_m.jpg" alt="Pearl sits on a scrap of garden fabric or something, bathed in a sunbeam"></a>
</div></p>
<p>And I have never known a cat to be quite so comfortable. Perhaps Anise, on occasion, but he doesn’t have the raw talent Pearl was born with.</p>
<p>You could tell she was settling in if she tucked her paws in against her chest, something she always did quite deliberately and distinctly. But that was only the first stage of comfort. If you were lucky, she would stretch out one arm really far, perhaps to place her paw on you. As she dozed off she might lay flat on her side with her limbs outstretched, which meant we always had to check blankets carefully for a flattened Pearl before sitting down. And if you were really lucky, you might witness Pearl in a chaos configuration, upside-down with her paws wherever.</p>
<p>But even just sitting up with her eyes closed, she looked so content. Looking at photos makes me want to take a nap with her.</p>
<p><div class="gallery">
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-lounge-arm.jpg" class="photo" title="Wow, Pearl! Cool arm. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-lounge-arm_m.jpg" alt="Pearl peeks out of a blanket with one arm outstretched and dangling a comically long distance downwards"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-lounge-bed-peek.jpg" class="photo" title="Living her best life. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-lounge-bed-peek_m.jpg" alt="Pearl's arm sticks out from under a blanket on the bed, while a peek of her face is barely visible"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-lounge-flat.jpg" class="photo" title="This is Pearl's "pork chop" configuration. If a blanket were over her she'd basically be invisible. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-lounge-flat_m.jpg" alt="Pearl sleeps on her side on a blanket, fairly flattened out"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-lounge-pillows.jpg" class="photo" title="Occasionally I would wake up with Pearl in my face like this. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-lounge-pillows_m.jpg" alt="Pearl lays in a funny pose tucked between two pillows, one of which Ash is sleeping on"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-content.jpg" class="photo" title="What a face. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-content_m.jpg" alt="Pearl makes a deeply contented expression"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-lounge-flop.jpg" class="photo" title="Cats are liquid. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-lounge-flop_m.jpg" alt="Pearl sleeps while flopped over someone's elbow"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-lounge-twist.jpg" class="photo" title="I have never been this comfortable in my whole life. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-lounge-twist_m.jpg" alt="Pearl's head pokes out from under a blanket, upside-down"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-lounge-inverted.jpg" class="photo" title="How do you sleep like this? Also her claw is caught on my shirt. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-lounge-inverted_m.jpg" alt="Pearl lays asleep on her back, head tucked against her shoulder, one paw outstretched towards the camera"></a>
</div></p>
<p>Sadly, Pearl had some health troubles from the start. She had a kink right at the base of her tail from the day we got her, suggesting it had been injured while at the breeder and not healed right, so she was never able to raise her tail all the way. She also came home with some sort of intestinal parasite that gave her a lot of… um, gastric distress, and while we were able to clear that up quickly, it seemed to recur soon afterwards.</p>
<p>We took her to the vet again, suspecting more parasites, but multiple tests turned up nothing. We tried a number of things — different food, sensitive-stomach food, wet food, more water, different treats — but could not seem to figure it out, and so Pearl just had stomachaches on and off for a while. Sometimes she would sit by the litterbox and grumble, and all I could do was try to reassure her.</p>
<p>It wasn’t until a few years later that Ash’s then-husband, with no explanation whatsoever, spontaneously decided to just feed her some plain chicken mixed with pumpkin purée. Just like that, she was fine. I felt like kind of an idiot for not trying that earlier, but after giving her <em>veterinary</em> sensitive-stomach food and seeing no change, I thought we’d ruled out food sensitivity.</p>
<p>We swiftly outlined a general idea of what Pearl could or could not tolerate. Chicken, pork, pumpkin: <span class="caps">OK</span>. Beef or any kind of organs: she immediately threw up. Fish: no good. And yet manufactured food containing only very simple things <em>still</em> gave her stomachaches, so our best guess was that she <em>also</em> couldn’t tolerate fucking xantham gum or something, which is in pretty much all pet food, including the sensitive stuff.</p>
<p>Regardless, we had a diet she could stomach, so for the rest of her life we made her a custom diet of ground chicken, ground pork belly, pumpkin, and some nutrient powder that didn’t bother her (which took several attempts to find). That meant no more free-feeding the other cats, so we got a big dog cage to keep the kibble in, and we’d let the other cats in there while Pearl was eating her special princess food. Thus began a multi-year saga during which, <em>every four hours</em>, like clockwork, Anise would start bothering me to feed him.</p>
<p>Please do not tell me what I could have done to dissuade Anise or space out the schedule. I guarantee, he is vastly more dastardly and annoying than you are giving him credit for. The cats run this household, and I have long since made peace with that.</p>
<p>The closest to any real insight we got about Pearl was that perhaps her kitten parasites had left her with <span class="caps">IBS</span> — a very vague diagnosis of exclusion, and the best anyone could come up with. But Pearl was happy, so that was good enough. We eventually found new treats she could stomach, too.</p>
<p><div class="gallery">
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-outside-boy-jail.jpg" class="photo" title="Pearl enjoys some scraps, while the other cats look on from within Boy Jail. Kibble Prison. The Purrnitentiary. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-outside-boy-jail_m.jpg" alt="Pearl eats some meat while three cats in a large dog cage stare at her"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-cage-sunbeam.jpg" class="photo" title="This looks like a promo ad for a reality show called The Kibble Joint. Hosted by Gorden Ramsay But He's A Cat. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-cage-sunbeam_m.jpg" alt="Pearl loafs in a sunbeam, right next to the empty cage"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-cats-crowding-ash.jpg" class="photo" title="Tiny vultures. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-cats-crowding-ash_m.jpg" alt="Four cats, one peeking over the rest from behind, crowd around Ash's dinner plate and look at it expectantly"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-rub-on-chair.jpg" class="photo" title="I don't have any more food-related photos of Pearl so please enjoy this one instead. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-rub-on-chair_m.jpg" alt="Pearl stands against the back of an office chair with a scratchy texture and intensely rubs her chin and cheek against it"></a>
</div></p>
<p>Pearl had relatively intense relationships with the other cats, much like she did with people.</p>
<p>She <em>adored</em> Napoleon, our furred and largest cat, for some reason. She often trotted up to him, very eager to sniff him; or when he trotted towards the kibble cage in recent years, she would run alongside him, staring sideways at him. I don’t really understand what her feelings were, and Napoleon didn’t really return them, but he at least tolerated them. Curiously, I can’t remember many attempts on Pearl’s part to snuggle up to Napoleon; she mostly snuggled with the other sphynxes.</p>
<p>She and Twigs (her uncle, incidentally) spent a ton of time together, and Anise was often in the mix as well. They’d often end up in a pile under or within a blanket, or all wedged into the same cat bed, or piled on a chair that had a towel on it. Sometimes she’d grumble at Anise for being too much in her personal space, but somehow Twigs’s presence seemed to defuse everything. I can’t remember her ever grumbling at Twigs, in fact.</p>
<p>Cheeseball is the only cat we have who’s younger than Pearl. When he was a kitten, she kind of doted on him like a mom, frequently trying to groom his head. She kept doing this into his adolescence, even as he was swiftly growing bigger than her, which was endearing and also very funny.</p>
<p><div class="gallery">
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-napoleon.jpg" class="photo" title="I don't have many photos of Pearl and Napoleon interacting, but this one of kitten Pearl seems to express it well. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-napoleon_m.jpg" alt="Napoleon and a comically smaller kitten Pearl share a cat bed"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-regard-napoleon-1.jpg" class="photo" title="Pearl regards Napoleon longingly from afar. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-regard-napoleon-1_m.jpg" alt="Pearl stares at Napoleon, who is in my lap"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-regard-napoleon-2.jpg" class="photo" title="Napoleon returns his feelings. I guess. I don't know what he's doing here. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-regard-napoleon-2_m.jpg" alt="Moments later, Napoleon has his outstretched paws resting on Pearl's back while she sleeps"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-twigs-lick-1.jpg" class="photo" title="Twigs tries to bathe Pearl, who is not having it. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-twigs-lick-1_m.jpg" alt="Twigs licks a kitten Pearl, who rolls on her back and waves her paws"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-twigs-lick-2.jpg" class="photo" title="Pearl makes her feelings expressly clear. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-twigs-lick-2_m.jpg" alt="Pearl pushes Twigs away with a paw"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-and-twigs.jpg" class="photo" title="They became fast friends, once Twigs forgave her betrayal of existing. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-and-twigs_m.jpg" alt="Pearl and Twigs nestle together comfortably in Ash's arms"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-under-twigs.jpg" class="photo" title="Twigs always did like sitting on cats, though. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-under-twigs_m.jpg" alt="Pearl lounges in a cat bed while Twigs props himself up on her"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-bite-anise.jpg" class="photo" title="He did nothing to deserve this. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-bite-anise_m.jpg" alt="Kitten Pearl bits Anise, who is clearly trying to sleep"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-twigs-lick-anise.jpg" class="photo" title="Pearl and Twigs both help give Anise a bath. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-twigs-lick-anise_m.jpg" alt="Three sphynxes lay wrapped in a blanket, Pearl and Twigs both licking the ears of Anise in the middle"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-trio.jpg" class="photo" title="The three of them often snuggled up somewhere together. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-trio_m.jpg" alt="Three sphynxes lay in a pile under a blanket or two"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-anise-sunbeam-1.jpg" class="photo" title="Pearl does not want to share this sunbeam with Anise. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-anise-sunbeam-1_m.jpg" alt="Pearl lays in a sunbeam with Twigs nearby; Anise has tried to approach and she is pushing him away with a sour expression"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-anise-sunbeam-3.jpg" class="photo" title="Twigs sacrifices his peace and quiet for the greater good. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-anise-sunbeam-3_m.jpg" alt="Pearl's paws are now on Twigs's face, though he doesn't seem bothered at all"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-anise-sunbeam-4.jpg" class="photo" title="The system reaches equilibrium. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-anise-sunbeam-4_m.jpg" alt="Pearl remains in the sunbeam and licks one paw, while Anise has settled a short distance away, facing the other way"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-lick-cheeseball.jpg" class="photo" title="This very large child is quite dirty. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-lick-cheeseball_m.jpg" alt="Pearl licks kitten Cheeseball's head, though he's already bigger than her"></a>
</div></p>
<p>We moved in 2018, and spent the summer with a former acquaintance’s parents, as they had a finished and furnished basement that was practically an apartment all on its own. Unfortunately, they had four cats of their own, for a total of <em>nine</em> crammed into a relatively small space. (One of the parents couldn’t be around cat hair in the medium term, due to reasons.)</p>
<p>One of the cats, Seamus, was a maine coon, and by all accounts kind of an asshole. He made a habit out of chasing Napoleon around, which Napoleon did not like at all, and which would result in Pearl chasing <em>him</em> to defend Napoleon, and then Anise chasing after Pearl because everyone is running around and he doesn’t quite understand why but he doesn’t want to be left out. We kept the cats separated as best we could, but we didn’t have much space to work with, and we were already trying to sequester Cheeseball, who we’d just adopted as a kitten. Everything was just kind of a mess.</p>
<p>Anyway this kinda stressed everyone out.</p>
<p>I bring it up because of one particular event. The only segregated parts of the basement were the bathroom and a somewhat awkwardly-shaped bedroom. The bedroom was <em>exclusively</em> for our cats. I don’t remember exactly what led up to this, but at some point Seamus made a beeline for the bedroom while Pearl was just inside the open door. I’m guessing Napoleon was in there too.</p>
<p>Pearl was <strong>absolutely</strong> not having this. She stood her ground and hissed hard enough to stop this absolutely massive cat in his tracks. She was so mad that she <em>peed on the floor</em> (which was, thankfully, vinyl). We got there to intervene about half a second later, but wow! She drew a line in the sand and under no circumstances was this bully going to cross it. We have always looked back fondly upon this “rage piss” incident.</p>
<p>I think Pearl was left a little rattled, though. Even at the time, she growled at the other maine coon there, who was an absolute sweetheart and rarely did more than sit nicely and ask to be pet. Once we were out of there, she seemed a little distrusting of Anise, often growling at him or biting his haunch merely for sitting nearby (which would entice a bewildered Anise into smacking her, justifying her reaction). I wish we hadn’t stayed there.</p>
<p>Cheeseball was also growing up and wanted to play with Pearl, because playing is how he engages with pretty much everything; alas, he was a bit too rowdy for Pearl. Twigs, infinitely patient, was there to absorb a lot of this.</p>
<p>But then <a href="proxy.php?url=https://eev.ee/blog/2019/10/26/goodbye-twigs/">Twigs died</a>, and the cats’ relationships seemed to deteriorate. Cheeseball liked Pearl, but he always wanted to fight with her, which she didn’t like. Anise liked Pearl, but she seemed to resent him a lot of the time, and there was no Twigs to separate them. Pearl liked Napoleon, but Napoleon liked to be by himself.</p>
<p>It was <em>okay</em>, but tense.</p>
<p>Maybe I’m overstating this. Going back through photos of Pearl, I’ve found plenty from the post-Twigs era where she’s still hanging out with Anise peacefully. A number of their conflicts even started because <em>she</em> would approach Anise to sit by him, <em>then</em> growl at him. No wonder he was confused. Sometimes she would groom him and start growling, <em>while licking his ear</em>. Hello? What are you doing?? What do you want from him here.</p>
<p>Still, that must mean she still liked him. She just had some complicated feelings. It always made me a little sad when they couldn’t get along, though. I’d gotten Anise in the first place in part to give Twigs a friend, and Pearl and Twigs had always gotten along well, and now… well.</p>
<p><div class="gallery">
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-under-anise.jpg" class="photo" title="She'd never let him get away with that if she didn't like him. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-under-anise_m.jpg" alt="Pearl under a blanket, with Anise stretched out and resting his paws on top of her"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-anise-cat-bed.jpg" class="photo" title="I do wonder which one was here first. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-anise-cat-bed_m.jpg" alt="Pearl and Anise share a cat bed"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-anise-vent.jpg" class="photo" title="There's a heater vent buried somewhere under there. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-anise-vent_m.jpg" alt="Pearl and Anise lay in a row, wedged between some boxes and the wall"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-anise-together.jpg" class="photo" title="Best of friends. For now. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-anise-together_m.jpg" alt="Pearl and Anise nap under a blanket, facing each other, with their arms kind of wrapped around each other"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-groomed-by-napoleon.jpg" class="photo" title="I guess he did like her back. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-groomed-by-napoleon_m.jpg" alt="Pearl tilts her head down while Napoleon licks the top of it"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-medley-on-ash.jpg" class="photo" title="This is just cat life. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-medley-on-ash_m.jpg" alt="Pearl, Anise, and Napoleon are all piled on top of Ash in bed"></a>
</div></p>
<p>Having said all this about how great and lovely Pearl is, her presumptuousness also made her a <em>huge pest</em> in some very specific ways. For example, once we’d settled into the food routine that saved her from constant stomachaces, one of her favorite things to do was to go over to the kibble cage and try to find kibble that had escaped from it. If she could get away with it, she would stick her paw between the bars and pull kibble (or the entire bowl) out to eat.</p>
<p>It was slightly annoying, and also very funny. We called this pulling a heist. And then she’d have awful gas some hours later.</p>
<p>I also very distinctly remembering getting takeout one time, which happened to include a breaded and fried slab of fish. I had the little takeout container on the table in front of me, and I think I was fiddling with the wrapper on their plastic fork or something, when Pearl came along, sniffed it… and then <em>bit the fish</em> and <em>pulled the whole filet out of the container</em>. Right in front of me! Points for boldness, I guess. She wasn’t <em>quite</em> so audacious any other time, but she must’ve really liked the smell of that fish.</p>
<p>And while she was generally pretty picky about what she would consider a toy, she did, on occasion, like to bite the arms of my glasses. Once I was laying next to her and petting her while she purred, and she stuck a paw in between my glasses and my face, <em>pulled them off</em>, and tried to bite them — purring all the while.</p>
<p><div class="gallery">
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-glasses.jpg" class="photo" title="Admittedly, I'm enabling her behavior. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-glasses_m.jpg" alt="Pearl takes a swipe at my glasses, which I am clearly offering to her"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-inspect-steak.jpg" class="photo" title="Twigs waits patiently to see if he will receive scraps. Anise peeks from behind. Pearl shoves her whole face in there. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-inspect-steak_m.jpg" alt="Three cats lurk, at various distances, around Ash, who is sitting on the couch and attempting to eat dinner"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-lollipop.jpg" class="photo" title="Pearl would like to investigate this, please, thank you, and nothing you do will stop her. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-lollipop_m.jpg" alt="Pearl straddling Ash's shoulder and outstretched hand to sniff a lollipop that Ash is trying to keep out of reach"></a>
</div></p>
<p>My favorite Pearl trick was what we dubbed “mouse alert”. If Pearl was looking for someone — often anyone at all, but sometimes a particular person who was absent or in a room with a closed door — she would find one of her toy mice and carry it around doing a very loud, muffled meow. If she saw you she would then drop the mouse and trot over, making happy high-pitched meows instead.</p>
<p>Sometimes she’d start out with regular meows, which we could hear from the other side of the house, but then they’d abruptly turn deeper and longer, and we knew she’d picked a mouse up. It was so charming and <em>so</em> funny. Every so often we’d find a pile of mice outside a door and we knew that Pearl had been trying to open it. She later expanded her roster to include Big Mouse — a plush almost half her size who became her favorite — and a plush of a single <span class="caps">HIV</span> virus that she must’ve stolen from my desk.</p>
<p>She didn’t play with the mice, either. I have video of her playing with <em>a</em> mouse when she was fairly young, but it’s not one of the mice we have now. She seemed to regard them as precious, her comforting belongings that she could almost always lure us out of hiding with. “Come look at my mouse!” Sometimes she’d carry them around quietly, just to have one or two nearby in a comfortable spot.</p>
<p>I tried for her <em>whole life</em> to get a recording of this, which proved nearly impossible, because she’d stop if she knew anyone was nearby! I got a clear recording only once, a week before she died; I was in our dark bedroom, filming into Ash’s office, and I don’t think she realized I was there. There’s a link at the bottom.</p>
<p>Her other favorite possession was string. Pearl <em>loved</em> to play string. She would ask for it by name. No, really. If she wanted to play string, she would find (or bring) a string and sit on it hoping someone noticed, and if that didn’t work, I’m pretty sure she had a specific meow for asking you to please follow her to string and then play with it.</p>
<p>Playing string with her was a slightly frustrating affair, but perhaps I just didn’t understand the rules. They <em>seemed</em> to be: I should wiggle the string; then Pearl grabs the string; then Pearl keeps the string. That doesn’t end the game, though. I should keep trying, in vain, to get the string back, while Pearl simply keeps winning.</p>
<p>A great thing to do was dangle it above her, at which point she’d stand up to try to get it and chomp at it, audibly. I loved her little chomp sound. I can’t even do it myself; I feel like I’d hurt my teeth.</p>
<p>After she was through adolescence, string was the only thing she really wanted to play with. She might’ve chased a laser pointer a couple of times, but string was the one thing she would ask for. Occasionally I’d try to play with Anise with a string, but Pearl had a fucking sixth sense for when string was happening, and she would appear from nowhere and go absolutely nuts over it while Anise sat back and watched.</p>
<p><div class="gallery">
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-string-please-1.jpg" class="photo" title="Please. Please may I have some string. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-string-please-1_m.jpg" alt="Pearl looks longingly at the camera while hunkered down atop a length of twine"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-string-please-2.jpg" class="photo" title="I would love any spare string you have. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-string-please-2_m.jpg" alt="Pearl looks longingly at the camera, again, with a length of string dangling from a dresser in the background"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-with-mouse.jpg" class="photo" title="She definitely put that mouse there herself. Also, I think this box was on top of a heater vent. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-with-mouse_m.jpg" alt="Pearl sits in a box with a small plush mouse next to her"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-swipe-string.jpg" class="photo" title="My shutter speed is not high enough for this. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-swipe-string_m.jpg" alt="Pearl swipes, blurrily, at a string dangling in front of her"></a>
</div></p>
<p>In March 2021, I took Pearl to an <span class="caps">ER</span> vet over very rapid breathing. They told me she’d had fluid in her lungs and diagnosed her with congestive heart failure. That’s when your heart can’t pump hard enough; part of Pearl’s heart wall had thinned and weakened, and one chamber was enlarged. She had to be hospitalized overnight. I drove home thinking I’d never see her again.</p>
<p>They couldn’t identify a cause. She was given a prognosis of “not fantastic” and prescribed a growing mountain of medication, which Ash dutifully gave to her every twelve hours for months on end, even when Pearl refused it. Sometimes Pearl had to be bribed with treats in order to eat at all, though I later traced that to a batch of food with insufficient pumpkin for her liking.</p>
<p>We had to keep her stress level low, which meant keeping her completely separated from the other cats (or at the very least Cheeseball) as much as possible. That meant Ash vanished into a closed room for most of every day to work while keeping an eye on Pearl — who was, after all, Ash’s cat. That also left me with three other cats constantly vying for my attention.</p>
<p>For several months we often couldn’t even sleep in the same room — Pearl and Anise couldn’t be left together, and Anise makes a racket all night if he’s shut out. Early on, our roommate would often take Pearl overnight (even despite being allergic to cats), but as time went on, Ash felt a stronger impulse to be around her as much as possible. Eventually we found we could have both Anise and Pearl overnight as long as we put a sweater on Anise and had sufficient extra blankets on the bed, but honestly it felt like a constant logistical nightmare.</p>
<p>Even with all this, we still had several more <span class="caps">ER</span> visits, several more hospitalizations.</p>
<p>Still, Pearl seemed to be doing okay. She was happy, she engaged with us, she purred, she snuggled, she nuzzled, she played. She was fine, and stable, until she wasn’t.</p>
<p>It was January 11, and it was the first <span class="caps">ER</span> visit for rapid breathing in a while. We handed her over, they hospitalized her, and we left, assuming we’d pick her up in the morning and she’d be fine, as had always happened.</p>
<p>We weren’t home for long before they called us. Pearl wasn’t recovering this time, and wouldn’t make it through the night.</p>
<p>We raced back. We saw Pearl, struggling to breathe, even on oxygen. We pet her and told her it would be okay. She cried out for help. Ash held her.</p>
<p>And then we let her go.</p>
<p><div class="gallery">
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-gone-mouse.jpg" class="photo" title="Pearl's mouse, right where she left it. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-gone-mouse_m.jpg" alt="A toy mouse lays atop a heating vent"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-gone-string.jpg" class="photo" title="Pearl's string, right where she left it. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-gone-string_m.jpg" alt="A bundle of string lays on the floor amidst some laundry"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-gone-corner.jpg" class="photo" title="Pearl's bathroom corner, waiting for her. I tidied it up a bit for her. You can see Big Mouse and Twigs's tiny mouse. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-gone-corner_m.jpg" alt="A nook surrounded by windows, with a towel in one corner and several plush toys neatly arranged on it"></a>
</div></p>
<p>I love and miss so many little things. She had such beautiful eyes, like Twigs did, though she squinted a lot so it always felt like a special treat when I could see them clearly. Her whole face scrunched when she meowed. She had a marble pattern, so I guess she would’ve been a calico. I didn’t even notice it when we first got her, and then one day it jumped right out at me and I felt briefly like our kitten had been replaced with a different one. She had a funny little clump of four hairs that stuck out from her hip. She had marbling on her pawpads, too.</p>
<p>I love her wide vocabulary of very cute little meows, in contrast with Twigs’s more raucous ones. She reserved them for special occasions, opting to chirr most of the time.</p>
<p>I love how, when she was surprised by something, she would simply jump straight up in the air an inch, then come down. No other movement. It was like she was tweened. I never tried to spook her on purpose to see this, but she was a little prone to being spooked.</p>
<p>I love how, when she’d knead at a soft blanket, she did just a few quick little motions and then she was done. It was so dainty. I always called it kitty paws, to distinguish from cat paws.</p>
<p>I love how she’d do a straight upwards stretch that somehow made her ears flick inside out briefly.</p>
<p>I love the very deliberate way she tucked her paws, and how she would gently hold onto someone’s shoulders while getting a taxi ride. Everything she did came across as so purposeful.</p>
<p>I love how Ash had found that rubbing their face on Pearl’s side as a kitten would get her to purr, and that kept working for her whole life, and it’s basically what she ended up doing to people in return.</p>
<p>I love how she had a funny obsession with water. I can’t really explain it, and I don’t know what she found so interesting. If I took a swig from my water bottle with Pearl nearby, she would climb on whatever was necessary to sniff at the nozzle. If I opened a soda with Pearl nearby, she’d stick her nose right in the opening, then recoil when the bubbles fizzed her. She didn’t enjoy baths or anything, she just liked… water. From afar. Like with Napoleon, perhaps.</p>
<p>I love how she nuzzled so hard that she hit maximum nuzzle, and so she would also sort of gently swipe the air with her paw as well, for extra nuzzling power.</p>
<p>I love her funny “bug off” sweater, illustrated with a ladybug, which seemed to capture her personality well: don’t be rude to me, but expressed in a very cute manner.</p>
<p>I love how she adopted the sort of extended windowsill in our bathroom as her own, and would lay there on sunny days and roll around on a towel.</p>
<p>I love that she was pampered right to the end. Over the course of recent weeks, Ash would keep giving me updates on Pearl’s development of a new routine, where she would sit in a Treat Spot she had designated, possibly meow once or twice, and wait very nicely until Ash gave her a treat. And then Ash would eventually capitulate, helpful before the polite ministrations of this very tiny cat, and give her a treat. It seemed that the number of treats Pearl was managing to get per day was gradually increasing, and so I asked every time: why not simply not give her a treat? But I knew the answer.</p>
<p>If you cried, there were decent odds that Pearl would come and comfort you, come chirp at you and nuzzle until you felt better.</p>
<p>When we first moved here, Ash’s ex-husband had driven the truck containing all our stuff, and he slept here one night before leaving for good. The day after he’d left, we heard Pearl doing mouse alert in the room he’d slept in, and I just broke down sobbing at the kitchen table, thinking about how Pearl liked him despite everything and was just trying to find him, and we had no way to tell her he wasn’t coming back or explain any of it to her. To her, one of her favorite people had just disappeared, and that was so sad.</p>
<p>But Pearl heard me, came over, jumped on the kitchen table, and purred and headbutted me like crazy. The idea that I was sad for her and she still wanted to comfort <em>me</em> made me cry harder.</p>
<p>She would also headbutt and nuzzle Ash specifically on the mouth when they sang, or do the same to me if I whistled competently. I suppose she liked music, but only from us.</p>
<p>Most of all, I love… how much she doted on Ash. She slept alongside them (me only a few times), she followed them around, she waited outside doors for them. They were her favorite person. I feel so bad for them, to have lost both Twigs and Pearl back to back.</p>
<p><div class="gallery">
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-water-1.jpg" class="photo" title="Sometimes she would climb all over me just to do this. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-water-1_m.jpg" alt="Pearl sniffs suspiciously at the nozzle of a reusable water bottle"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-water-2.jpg" class="photo" title="I wonder what she's thinking. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-water-2_m.jpg" alt="Pearl stares down into a half-full bottled water"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-wait-outside-door.jpg" class="photo" title="Guess who's in the bathroom. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-wait-outside-door_m.jpg" alt="Pearl sits patiently outside the bathroom door"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-bug-off.jpg" class="photo" title="Pearl's favorite shirt. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-bug-off_m.jpg" alt="Pearl seen from above to be wearing a green and white striped shirt with "BUG OFF!" and a ladybug emblem"></a>
</div></p>
<p>It’s been… two weeks now. Just over, because it took me another day to finish this post.</p>
<p>I don’t know if it’s fully clicked yet. I didn’t see Pearl much during the day, since she’d be tucked away in Ash’s office slash our bedroom. I saw her mostly at night and first thing in the morning. So while I’m out here, at my desk, it’s like nothing has changed. It only sinks in when I go upstairs and see the door left open, see a bed with no Pearl tucked in it somewhere.</p>
<p>It’s kind of dumbfounding just how much of this house and our lives had warped around Pearl, around this one tiny cat who loved everyone. So many things have disappeared or seem superfluous now. I was already free-feeding the other cats again since Pearl wasn’t allowed to roam the house unsupervised, but now we don’t need the kibble cage at all. Half our doors had been kept closed to make a few different places for Pearl to stay, but now none of that is necessary. Litterboxes had ended up scattered throughout the house so Pearl would always have access to one; now they’re back to being in a few central locations.</p>
<p>Ash doesn’t have to wake up at a specific time every day to give Pearl medicine. Pearl won’t wake us up to feed her. We don’t have to make her food, ever again.</p>
<p>And there are so many things that were only for Pearl. This wasn’t the case for anyone else. <a href="proxy.php?url=https://eev.ee/blog/2013/04/30/goodbye-styx/">Styx</a> only had communal cat sweaters; his favorite toy was loose change on my desk. Twigs, too, only had sweaters that Anise and Pearl inherited; his one dedicated toy was a single very tiny mouse he sometimes played with.</p>
<p>But Pearl? Half the sweaters we have <em>only</em> fit Pearl. Her mice were very much hers. Even her string was very much hers. We have a mortar and pestle that were specifically for grinding up her medication, oral syringes only Pearl used. She had possessions of her very own, things she’s left behind.</p>
<p>We knew this was coming, of course. Without the intervention of modern medicine, she would have died last March, and the outlook for heart failure in a cat isn’t great. I’ve already grieved for her several times over the past year. I didn’t see her much during the summer, but I’d been trying to spend more deliberate time with her in recent months, and I’m glad I did. I regret nothing. I earned her purrs, I played string with her exactly the right amount, I woke up to her stealing my pillow. I got the full Pearl experience.</p>
<p>And so did she. Ash took her outside extra over the summer, let her see a bit of the outside world (even if it was only our yard). We let her roam the house when we could, banishing Cheeseball to a room by himself if necessary, though she usually ended up sitting on a vent or my lap (or trying to heist some kibble). She got lots of treats, lots of love, lots of blankets, and even a vent all to herself. What more could she ask for?</p>
<p>She was living on borrowed time, but we borrowed every second we could. I don’t know what else we could’ve done. And we were there for her right up until the end. We didn’t have that opportunity with Twigs; he died in the back room, surrounded by strangers.</p>
<p>In the end, her heart was literally too big.</p>
<p><div class="gallery">
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-judging.jpg" class="photo" title="I feel like she's judging me. Or maybe judging my shoes. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-judging_m.jpg" alt="Pearl sits, paws folded, looking downwards with a frumpy frowny expression"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-stretch-on-ash.jpg" class="photo" title="Don't mind me. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-stretch-on-ash_m.jpg" alt="Pearl does a big stretch, straight upwards, while standing on Ash's chest, as they try to look at their phone"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-anise-tv.jpg" class="photo" title="Pearl and Anise catch the latest episode of their favorite drama, about a squirrel, who may or may not get got. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-anise-tv_m.jpg" alt="Pearl and Anise stare at a video of a squirrel on the TV"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-standing.jpg" class="photo" title="This looks so fucking weird to me every time I see it?? "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-standing_m.jpg" alt="Pearl stands up on her hind legs, making her shoulders look very strange"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-from-below.jpg" class="photo" title="Beautiful. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-from-below_m.jpg" alt="Pearl's face as seen from a low angle"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-meow.jpg" class="photo" title="I caught her in mid-meow here, which is just miraculous timing. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-meow_m.jpg" alt="Pearl's face is scrunched as she meows"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-eevee-cushion.jpg" class="photo" title="That's my Eevee cushion. My favorite genre is Pearl settled on things that look like you extruded Pearl's shadow outwards. "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-eevee-cushion_m.jpg" alt="Pearl sits with tucked paws on a little pillow that's only a little bigger than she is"></a>
<a href="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-ash-shoulder.jpg" class="photo" title="Is she sleeping? How does she do that? "><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-ash-shoulder_m.jpg" alt="Pearl appears to nap, laying across Ash's shoulders while they draw"></a>
</div></p>
<p>This sucks.</p>
<p>Pearl deserved better. She was dealt a bad hand from the beginning, but she was still friendly and kind, and then this happened. She was so young, too — her eighth birthday would’ve been next month. She, like Twigs, should’ve had twice as long.</p>
<p>Things won’t be difficult for her any more, I guess. I don’t know how much that comforts me.</p>
<p>Everything else moves on. Pearl continued until the night of January 11, 2022, but can go no further. We’re forced to leave her there, retaining only memories, while time carries us gently forward, ever further away.</p>
<p>So here is my landmark, my stake in the ground. Pearl was here. May this mark out the shape of who she was and leave that impression upon the world for much longer.</p>
<p>The finality of death resolves so many questions. I often wished I could improve Pearl’s tense relationships with Anise and Cheeseball, but now there’s no problem to solve. The interactions they had are all the interactions they will ever have. The tension is gone, now. The worries about how long Pearl’s heart will last are gone too.</p>
<p>The cat dynamic has shifted, again. Cheeseball and Napoleon have been much more affectionate towards Ash, and Napoleon has suddenly become a lap cat. I suppose the rest of the cats missed Ash while they were siloed away with Pearl for so long. Maybe they’re grieving? Cats are so open with their emotions, but sometimes they’re still inscrutable.</p>
<p>Pearl’s urn is on the dresser in our bedroom, right next to Twigs. Hers is bigger than his, somehow. But that’s Pearl for you — she always knew how to take up space.</p>
<p>…</p>
<p>No, this is too dire an ending. Pearl was dealt a bad hand, but she always tried to be nice despite that. She got to see a lot of places and make a lot of friends, both people and cats and even one dog. Even when she had complex and skeptical feelings about Anise, she kept trying to be friends with him. She faltered at times, but she always did her best to uphold her principles of loveliness, strong boundaries, and please give me a treat.</p>
<p>That’s a lot for a tiny cat. I admire her for it, and I will not forget it.</p>
<p><div class="prose-full-illustration"><img src="proxy.php?url=/media/2022-01-goodbye-pearl/pearl-ash-desk.jpg" alt="Pearl sitting contently next to Ash at their desk"></div></p>
<p>Thank you for reading about Pearl. I hope you’ll remember her too. We loved her very much, and she put a lot of love back into the world. If you would like to experience more Pearl, here are some videos of her. I have some more to sift through, so this list may grow in the coming days.</p>
<ul>
<li>
<p><a href="proxy.php?url=https://www.youtube.com/watch?v=iVBUsFLXknE">Pearl loves Apollo, our former German Shepherd</a></p>
</li>
<li>
<p><a href="proxy.php?url=https://www.youtube.com/watch?v=j2hj84IkT6A">Kitten Pearl meows and purrs</a></p>
</li>
<li>
<p><a href="proxy.php?url=https://www.youtube.com/watch?v=m3NhZHr-E0I">Pearl performs mouse alert</a></p>
</li>
<li>
<p><a href="proxy.php?url=https://www.youtube.com/watch?v=WoQxHUvBQ1o">Pearl attempts to open the kibble tub with her face</a></p>
</li>
<li>
<p><a href="proxy.php?url=https://www.youtube.com/watch?v=u1DmIYWKSsU">Pearl asks very clearly for string, without even saying a word</a></p>
</li>
<li>
<p><a href="proxy.php?url=https://www.youtube.com/watch?v=bd4Bvdz-qMc">Pearl rolls around in a sunbeam</a></p>
</li>
</ul>
<p>And here are some games she has starred in. Or, rather, her fursona Purrl has starred in them.</p>
<ul>
<li>
<p><a href="proxy.php?url=https://eevee.itch.io/neon-phase"><span class="caps">NEON</span> <span class="caps">PHASE</span></a> — a shortish platformer with items and puzzles where Purrl is an <span class="caps">NPC</span></p>
</li>
<li>
<p><a href="proxy.php?url=https://eevee.itch.io/lunar-depot-38">Lunar Depot 38</a> — a little platforming shooter where you play as Purrl, with a gun</p>
</li>
<li>
<p><a href="proxy.php?url=https://eevee.itch.io/anise-escape-despair">Star Anise Chronicles: Escape from the Chamber of Despair</a> — an illustrated text adventure where you must work together with Purrl to escape</p>
</li>
<li>
<p><a href="proxy.php?url=https://eevee.itch.io/anise-wheres-twig">Star Anise Chronicles: Oh No Wheres Twig??</a> — a short platformer with items and puzzles where Purrl is an <span class="caps">NPC</span>, but differently, and made after Twigs died</p>
</li>
<li>
<p><a href="proxy.php?url=https://eevee.itch.io/a-very-tiny-purrl-game">a very tiny purrl game</a> — a tiny 3D platformer where you play as Purrl and must collect things</p>
</li>
</ul>Recommended GZDoom settings2021-12-11T18:58:00-08:002021-12-11T18:58:00-08:00Eeveetag:eev.ee,2021-12-11:/blog/2021/12/11/recommended-gzdoom-settings/<p><a href="proxy.php?url=https://zdoom.org/index">GZDoom</a> is the fanciest way to play Doom. Unfortunately, it has also historically been difficult to recommend to newcomers, because its default settings are… <em>questionable</em>.</p>
<p>Conspicuously, for over a decade, it defaulted to traditional Doom movement keys (no <span class="caps">WASD</span>) and no mouselook. I am <em>overjoyed</em> to discover that this is no longer the case, and it plays like a god damn <span class="caps">FPS</span> out of the box, but there are still a few twiddles that need twiddling. Mostly the texture filtering. Christ, the texture filtering.</p>
<p>Anyway GZDoom has a lot of options, so here is a handy list of the important ones. There are fewer than I expected, which is good.</p>
<p><a href="proxy.php?url=https://zdoom.org/index">GZDoom</a> is the fanciest way to play Doom. Unfortunately, it has also historically been difficult to recommend to newcomers, because its default settings are… <em>questionable</em>.</p>
<p>Conspicuously, for over a decade, it defaulted to traditional Doom movement keys (no <span class="caps">WASD</span>) and no mouselook. I am <em>overjoyed</em> to discover that this is no longer the case, and it plays like a god damn <span class="caps">FPS</span> out of the box, but there are still a few twiddles that need twiddling. Mostly the texture filtering. Christ, the texture filtering.</p>
<p>Anyway GZDoom has a lot of options, so here is a handy list of the important ones. There are fewer than I expected, which is good.</p>
<hr />
<p>Note that the routes given to the various settings are for the <em>full</em> options menu. Out of the box, GZDoom shows a reduced options menu, <em>because it has a lot of options</em>. You can get to the full menu from <code>Full options menu</code> near the bottom, and from there turn off the simple menu (if you want). If you get lost, you can also use the option search.</p>
<p>Also, virtually every setting in GZDoom takes effect <em>instantly</em>, even while the menu is still visible. (That’s why there are no screenshots here! Just try stuff out yourself.) It remembers where your cursor was, too, so you can exit the menu to try stuff out, then bring it back up and mash Enter a few times to get back to where you were.</p>
<h2 id="absolute-necessities"><a class="toclink" href="proxy.php?url=#absolute-necessities">Absolute necessities</a></h2>
<p>I do not understand how anyone could argue with these.</p>
<ul>
<li>
<p><strong>Disable texture filtering.</strong></p>
<p><code>Display options > Texture options > Texture filter mode: None (linear mipmap)</code></p>
<p>By default, GZDoom uses linear upscaling on all sprites and textures, turning them into a blurry mess. This is objectively ludicrous, since the sprites and textures are <em>pixel art</em>.</p>
<p><code>None</code> restores the crispy aesthetic that God intended — and when I say God, I of course mean John Carmack. No, wait, maybe I mean Adrian Carmack?</p>
<p>The “linear mipmap” bit means that GZDoom will still use linear <em>downscaling</em>, so that distant textures still somewhat resemble the actual texture and do not simply collapse into a pixel of arbitrary color. If you find this objectionable, you may of course simply set it to <code>None</code>.</p>
</li>
<li>
<p><strong>Fix the lighting.</strong></p>
<p><code>Display options > Hardware renderer > Sector light mode: Software</code></p>
<p>GZDoom has half a dozen different lighting models (for… some reason), all of which are way off from how Doom actually looked, except for this one.</p>
</li>
<li>
<p><strong>Fix the partial invisibility effect.</strong></p>
<p><code>Display options > Hardware renderer > Fuzz style: Software</code></p>
<p>GZDoom defaults to rendering spectres (the harder-to-see variants of the pink demons) with a sort of translucent effect, which is <em>easier</em> to see, which sort of defeats the purpose of making them harder to see.</p>
<p>This will emulate the appearance of the original game, scaled up to big chunky pixels. I actually prefer <code>Smooth fuzz</code>, which fits better at high resolutions and still looks like a rendering error, but pretty much anything is better than the <code>Shadow</code> default.</p>
<p>For testing purposes, it may help to pop open the console with the backtick key (top left) and type <code>summon spectre</code> to… well, summon a spectre.</p>
</li>
</ul>
<p>And if all you want is something that looks kinda like Doom, you’re done! Feel free to stop reading here.</p>
<p>If you’re pickier…</p>
<h2 id="my-own-preferences"><a class="toclink" href="proxy.php?url=#my-own-preferences">My own preferences</a></h2>
<p>These are also all correct.</p>
<ul>
<li>
<p><strong>Always run.</strong></p>
<p><code>Player setup > Always run: on</code></p>
<p>I don’t know why you would walk anywhere in Doom. We’re in a fucking hurry, man. There are <em>demons</em>.</p>
<p>While you’re here, you may want to set your <code>gender</code> as appropriate to fix pronouns in obituary messages. You can also turn autoaim down, or off.</p>
</li>
<li>
<p><strong>Show a crosshair.</strong></p>
<p><code>HUD options > Default crosshair: Cross 2</code><br />
<code>HUD options > Scale crosshair: 0.00</code></p>
<p>I just feel better with a little symbol in the middle of the screen. I’m holding all my guns at chest height, for some reason, so the sights on those are useless.</p>
<p>By default the crosshair is humongous, though, hence the scaling.</p>
</li>
<li>
<p>Speaking of which, <strong>fix the <span class="caps">HUD</span> scale.</strong></p>
<p><code>HUD options > Scaling options > User interface scale: 3</code></p>
<p>The automatic setting is <em>okay</em> (and better than it used to be), but still leaves some things like pickup messages and the console as microscopic. I play in a 1080p window on a 1440p monitor, and this seems nice for me. Adjust as desired.</p>
</li>
<li>
<p><strong>Use the alternative <span class="caps">HUD</span>.</strong></p>
<p><code>HUD options > Alternative HUD > Enable alternative HUD: On</code></p>
<p>You’ll need to press <kbd>+</kbd> until the status bar disappears to actually see it.</p>
<p>The alternative <span class="caps">HUD</span> shows you everything you need to know about the state of the game, while consuming minimal space and still letting you see the weapon sprites in their full glory. It also shows you a count of kills and secrets, so you have some idea of the progress you’ve made. <em>And</em> it tells you a few things that you had to keep track of yourself in vanilla Doom, like what color of armor you have and whether you have the berserk fist.</p>
<p>(This replaces a stock fullscreen-with-info <span class="caps">HUD</span> that didn’t exist in vanilla Doom, but which only shows you health, armor, keys, and ammo for your current weapon. Note that if you play a <span class="caps">WAD</span> that heavily alters the game, there’s a chance it will add custom stuff to the stock <span class="caps">HUD</span>, and that stuff <em>will not appear</em> on the alternative <span class="caps">HUD</span>. It’s explicitly not moddable.)</p>
</li>
<li>
<p><strong>Draw shadows in corners.</strong></p>
<p><code>Display options > Hardware renderer > Postprocessing > Ambient occlusion quality: Medium</code></p>
<p>Doom has static lighting that affects the walls and floor equally, so the transition from wall to floor/ceiling is pretty flat. A little <span class="caps">AO</span> helps that stand out, even if ambient occlusion is a fake idea.</p>
</li>
<li>
<p><strong>Fix fake contrast.</strong></p>
<p><code>Display options > Use fake contrast: Smooth</code></p>
<p><span class="dquo">“</span>Fake contrast” refers to a clever trick in the Doom engine wherein horizontal (as seen on the automap) walls draw darker than the room, and vertical walls draw lighter. In rectangular rooms, this helps avoid the “flat” feeling mentioned previously.</p>
<p>Unfortunately, with complex geometry — as you see frequently in modern maps, but also occasionally in the original ones — this can backfire. I’ve been fooled into thinking one particular wall in a curved hallway is a secret, just because it happened to be vertical and appeared lighter than its neighbors. Meanwhile, rooms at a slant don’t benefit at all.</p>
<p><code>Smooth</code> preserves the effect, but gradually transitions between the original effect for orthogonal walls and normal lighting for walls at a 45° angle. (That is, a wall at a 22.5° angle will have half the fake contrast effect.)</p>
</li>
<li>
<p><strong>Turn on antialiasing.</strong></p>
<p><code>Display options > Hardware renderer > Postprocessing > FXAA quality: Low</code></p>
<p>This smooths out lines in the geometry (or straight horizontal lines in textures) when drawn at an angle, without sacrificing those crunchy pixels.</p>
</li>
<li>
<p><strong>Use particles.</strong></p>
<p><code>Display options > Hardware renderer > Particle style: Round</code><br />
<code>Display options > Rocket trails: Particles</code><br />
<code>Display options > Blood type: Sprites & particles</code><br />
<code>Display options > Bullet puff type: Sprites & particles</code></p>
<p>The default particles are linear filtered, which looks awful, but I don’t think anything uses particles by default so you’d never notice. You can also set them to <code>Square</code>, but I think having a single pixel floating in the air looks a bit silly.</p>
<p>Adding particles to blood splatters and bullet puffs just looks nice. I replace the rocket trails entirely because the original Doom rocket cloud is just kinda big and clumsy and ugly.</p>
</li>
<li>
<p><strong>Enable dynamic lighting.</strong></p>
<p>This is on by default… sort of. GZDoom needs to be able to find the <code>lights.pk3</code> and <code>brightmaps.pk3</code> files bundled with it, but if it runs at all, it probably knows where they are.</p>
<p>So all you have to do is check <code>Load lights</code> and <code>Load brightmaps</code> in the little dialog you get when launching the game.</p>
<p><em>Probably</em>. See, for some reason, those checkboxes are only there on Windows — in fact, I didn’t know they existed at all until two minutes ago. Even though they set a config setting, they aren’t accessible via the options menu. So if that doesn’t work for you for whatever reason, try popping open the console and doing:</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span>autoloadlights true
<span class="linenos">2</span>autoloadbrightmaps true
</code></pre></div>
<p>Then restart the game. Glowing objects should now cast (fairly subtle!) light on nearby walls. You can see this immediately in Doom <span class="caps">II</span>’s first map — there should be a green glow on the floor underneath the armor bonus in the far right corner of the room. Or for a more dramatic demonstration, <code>IDKFA</code> and fire a rocket.</p>
<p>It’s just a nice touch. And unlike many attempts to add dynamic lighting to Doom, it’s not so over-the-top as to be distracting.</p>
</li>
</ul>
<h2 id="for-the-extremely-ornery"><a class="toclink" href="proxy.php?url=#for-the-extremely-ornery">For the extremely ornery</a></h2>
<p>At the other end of the scale, there are those who want an experience as close as possible to vanilla Doom. Those people might just want to use a port closer to vanilla, like a PRBoom variant or even Chocolate Doom, but GZDoom is willing to do its best:</p>
<ul>
<li>
<p><strong>Quantize light levels.</strong></p>
<p><code>Display options > Hardware renderer > Banded SW lightmode: On</code></p>
<p>Doom maps support light levels from 0 to 255, but in practice, Doom only understood… 16, I think? That’s because it was a paletted game, and it needed a colormap telling it how to darken each color while still sticking to the palette. The game only shipped with 15 such mappings, probably because 255 of them would have been ludicrous, and thus there are only 16 light levels in practice.</p>
<p>GZDoom’s hardware renderer isn’t bound by a palette, so it happily supports all 256 light levels. If you can’t stand this, well, it can simulate 16 for you.</p>
</li>
<li>
<p><strong>Disable truecolor.</strong></p>
<p><code>Display options > Hardware renderer > Postprocessing > Tonemap mode: Palette</code></p>
<p>Doom was a paletted game and only ever displayed the same 256 colors. You can make the hardware renderer emulate this effect if you really want to. I don’t know why you would want to.</p>
<p>It won’t be <em>exactly</em> the same, of course; Doom’s palette-mapping was handcrafted, whereas the renderer is doing it automatically.</p>
</li>
<li>
<p><strong>Disable the hardware renderer altogether.</strong></p>
<p><code>Set video mode > Render mode: True color SW renderer</code></p>
<p>If the very notion of accelerated rendering offends you, the original core of Doom’s renderer is still in there, just waiting for you. All you need do is turn it on. Note that this will severely restrict your ability to mouselook and will draw without vertical perspective, as the Doom renderer was designed around drawing vertical lines.</p>
<p>What’s that? Even true color is too much? You need the paletted glory that was the best a 386 could do? Well, <code>Doom software renderer</code> is also an option.</p>
</li>
<li>
<p><strong>Disable mouselook.</strong></p>
<p><code>Mouse options > Always mouselook: Off</code></p>
<p>Doom didn’t support looking up and down. Why should you?</p>
<p>Despite the name, this still allows you to look around <em>horizontally</em>. I guess technically that’s turning, not looking. Also, moving the mouse up and down will now move you forwards or backwards, just as in vanilla Doom.</p>
</li>
<li>
<p><strong>Disable <span class="caps">WASD</span>.</strong></p>
<p><code>Customize controls > Preferred keyboard layout: Classic ZDoom</code>, then <code>Reset to defaults</code></p>
<p>Okay now you have gone too far. This restores the very keyboard bindings I wanted to rally against — arrow keys to move, turning by default, <kbd>Alt</kbd> to strafe…</p>
</li>
<li>
<p><strong>Disable teleporter zoom.</strong></p>
<p><code>Display options > Teleporter zoom: Off</code></p>
<p>GZDoom does a brief zoom-in effect on your field of view after (non-silent) teleporting. Looks sick. If you hate it, here’s how to turn it off.</p>
</li>
<li>
<p><strong>Restore the vanilla lite-amp goggles.</strong></p>
<p><code>Display options > Hardware renderer > Enhanced night vision mode: Off</code></p>
<p>In vanilla Doom, the lite-amp goggles simply make the entire world render as fullbright, which looks fucking terrible. GZDoom defaults to a “night vision goggles” sort of effect that also highlights objects, but if you really can’t stand that, this twiddle is here for you.</p>
</li>
<li>
<p><strong>Enable randomized pitch on sound effects.</strong></p>
<p><code>Sound options > Randomize pitches: On</code></p>
<p>For the <em>very</em> ornery, I believe this behavior was in the original release of Doom but (accidentally?) broken in Doom 1.2 and all later versions. It’s really weird, but it’s the intended behavior, I guess!</p>
</li>
<li>
<p><strong>Restore Doom’s automap colors.</strong></p>
<p><code>Automap options > Map color set: Traditional Doom</code></p>
<p>This will change the automap back to its red-and-yellow-on-black glory.</p>
<p>It will also remove the colors that tell you where locked doors and the exit are. You might argue that those are cheating. I argue that they are the entire point of a map.</p>
<p>You can also turn off the automap’s monster and secret counts here if you truly wish to be as lost as possible.</p>
</li>
<li>
<p><strong>Twiddle with compatibility settings.</strong></p>
<p><code>Compatibility options > Compatibility mode: ?</code></p>
<p>You might want <code>Doom (strict)</code> for the closest vanilla experience that GZDoom can provide. <em>Might</em>. The most notable effects are:</p>
<ul>
<li>Monsters will wake up when seeing a player with a blur sphere. By default, they usually won’t, a behavior inherited from Hexen.</li>
<li>Arch-viles can resurrect crushed corpses as “ghosts” that cannot be shot, only harmed by splash damage from rockets.</li>
<li>Pain elementals will be unable to spawn new lost souls if there are at least 21 already present in the level.</li>
<li>Monsters can’t be knocked off of high ledges.</li>
<li>You will be unable to crowdsurf, meaning you will be blocked both by imps at the foot of a cliff below you, and by cacodemons flying above you.</li>
</ul>
<p>You can also toggle these on or off individually at your leisure.</p>
</li>
</ul>Gamedev from scratch 1: Scaffolding2021-01-26T18:27:00-08:002021-01-26T18:27:00-08:00Eeveetag:eev.ee,2021-01-26:/blog/2021/01/26/gamedev-from-scratch-1-scaffolding/<p>Welcome to part 1 of this narrative series about writing a complete video game from scratch, using the <span class="caps">PICO</span>-8. This is actually the second part, because in this house (unlike Lua) we index from 0, so if you’re new here you may want to consult the introductory stuff and table of contents in <a href="proxy.php?url=https://eev.ee/blog/2020/11/30/gamedev-from-scratch-0-groundwork/">part zero</a>.</p>
<p>If you’ve been following along, welcome back, and let’s dive right in!</p>
<p>Welcome to part 1 of this narrative series about writing a complete video game from scratch, using the <span class="caps">PICO</span>-8. This is actually the second part, because in this house (unlike Lua) we index from 0, so if you’re new here you may want to consult the introductory stuff and table of contents in <a href="proxy.php?url=https://eev.ee/blog/2020/11/30/gamedev-from-scratch-0-groundwork/">part zero</a>.</p>
<p>If you’ve been following along, welcome back, and let’s dive right in!</p>
<p><strong><a href="proxy.php?url=https://eev.ee/blog/2020/11/30/gamedev-from-scratch-0-groundwork/">← Part 0: Groundwork</a></strong></p>
<h2 id="recap-and-short-term-plans"><a class="toclink" href="proxy.php?url=#recap-and-short-term-plans">Recap and short-term plans</a></h2>
<p>So far, I have… this. Which is something, and certainly much more than nothing, but all told not a lot.</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/gamedev-from-scratch/v1-anise-moving3.gif" alt="Star Anise walking around the screen and turning to face the way he's moving">
</div>
<p>Most conspicuously, this is going to be a <em>platformer</em>, so I need gravity. The problem with gravity is that it means things are always moving downwards, and if there’s nothing to stop them, they will continue off indefinitely into the void.</p>
<p>What I am trying to say here is that I feel the looming spectre of collision detection hanging over me. I’m going to need it, and I’m going to need it <em>real soon</em>.</p>
<p>And, hey, that sucks. Collision detection is a real big pain in the ass to write, so needing it this early is a hell of a big spike in the learning curve. Luckily for you, someone else has already written it: me!</p>
<p>Before I can get to that, though, I need to add some structure to the code I have so far. Everything I’ve written is designed to work for Star Anise <em>and only</em> Star Anise. That’s perfectly fine when he’s the only thing in the game, but I don’t expect he’ll stay alone for long! Collision detection in particular is a pretty major component of a platformer, so I definitely want to be able to reuse it for other things in the game. Also, collision detection is a big fucking hairy mess, so I definitely want to be able to shove it in a corner somewhere I’ll never have to look at it again.</p>
<p>A good start would be to build towards having a corner to shove it into.</p>
<h2 id="adding-some-structure"><a class="toclink" href="proxy.php?url=#adding-some-structure">Adding some structure</a></h2>
<p>As of where I left off last time, my special <code>_update()</code> and <code>_draw()</code> functions are mostly full of code for updating and drawing Star Anise. That doesn’t really sit right with me; as the main entry points, they should be about updating and drawing <em>the game itself</em>. Star Anise is <em>part of</em> the game, but he isn’t the whole game. All that code that’s specific to him should be put off in a little box somewhere. Cats love to be in little boxes, you see.</p>
<p>This raises the question of how I want to structure this project in general. And, I note: structuring a software project is <em>hard</em>, and you only really get a good sense of how to do it from experience. I’m still not sure <em>I</em> have a good sense of how to do it. Hell, I’m not convinced <em>anyone</em> has a good sense of how to do it.</p>
<p>Thankfully, this is a game, so it’s pretty obvious how to break it into pieces. (The tradeoff is that everything in a game ends up entangled with everything else no matter how you structure it, alas.) Star Anise is a separate <em>thing</em> in the game, so he might as well be a separate <em>thing</em> in the code. Later on I’ll need some more abstract structuring, but as an extremely rough guideline: if I can give it a name, it’s a good candidate to be made into a <em>thing</em>.</p>
<p>But what, exactly, is a <em>thing</em> in code? Most commonly (but not always), a <em>thing</em> is implemented with what’s called an <em>object</em> — a little bundle of data (what it <em>is</em>) with code (what it can <em>do</em>). I already have both of these parts for Star Anise: he has data like his position and which way he’s facing, and he has code for doing things like updating or drawing himself. A great first step would be to extract that stuff into an object, after which some other structure might reveal itself.</p>
<p>I do need to do one thing before I can turn get to that, though. You see, Lua is one of the few languages in common use today that doesn’t <em>quite</em> have built-in support for objects. Instead, it has all the building blocks you need to craft your own system for making objects. On the one hand, the way it does that is very slick and clever. On the other hand, it means you can’t write much Lua without cobbling together some arcane nonsense first, and also no one’s code quite works the same way.</p>
<p>Which brings me to the following magnificent monstrosity:</p>
<div class="highlight"><pre><span></span><code><span class="linenos"> 1</span><span class="kr">function</span><span class="w"> </span><span class="nf">nop</span><span class="p">(...)</span><span class="w"> </span><span class="kr">return</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="kr">end</span>
<span class="linenos"> 2</span>
<span class="linenos"> 3</span><span class="c1">--------------------------------</span>
<span class="linenos"> 4</span><span class="c1">-- simple object type</span>
<span class="linenos"> 5</span><span class="kd">local</span><span class="w"> </span><span class="nv">obj</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="nv">init</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">nop</span><span class="p">}</span>
<span class="linenos"> 6</span><span class="nv">obj</span><span class="p">.</span><span class="py">__index</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">obj</span>
<span class="linenos"> 7</span>
<span class="linenos"> 8</span><span class="kr">function</span><span class="w"> </span><span class="nc">obj</span><span class="p">:</span><span class="nf">__call</span><span class="p">(...)</span>
<span class="linenos"> 9</span><span class="w"> </span><span class="kd">local</span><span class="w"> </span><span class="nv">o</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">setmetatable</span><span class="p">({},</span><span class="w"> </span><span class="nv">self</span><span class="p">)</span>
<span class="linenos">10</span><span class="w"> </span><span class="kr">return</span><span class="w"> </span><span class="nv">o</span><span class="p">,</span><span class="w"> </span><span class="nv">o</span><span class="p">:</span><span class="nf">init</span><span class="p">(...)</span>
<span class="linenos">11</span><span class="kr">end</span>
<span class="linenos">12</span>
<span class="linenos">13</span><span class="c1">-- subclassing</span>
<span class="linenos">14</span><span class="kr">function</span><span class="w"> </span><span class="nc">obj</span><span class="p">:</span><span class="nf">extend</span><span class="p">(</span><span class="nv">proto</span><span class="p">)</span>
<span class="linenos">15</span><span class="w"> </span><span class="nv">proto</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">proto</span><span class="w"> </span><span class="ow">or</span><span class="w"> </span><span class="p">{}</span>
<span class="linenos">16</span>
<span class="linenos">17</span><span class="w"> </span><span class="c1">-- copy meta values, since lua doesn't walk the prototype chain to find them</span>
<span class="linenos">18</span><span class="w"> </span><span class="kr">for</span><span class="w"> </span><span class="nv">k</span><span class="p">,</span><span class="w"> </span><span class="nv">v</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nb">pairs</span><span class="p">(</span><span class="nv">self</span><span class="p">)</span><span class="w"> </span><span class="kr">do</span>
<span class="linenos">19</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nf">sub</span><span class="p">(</span><span class="nv">k</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s2">"__"</span><span class="w"> </span><span class="kr">then</span>
<span class="linenos">20</span><span class="w"> </span><span class="nv">proto</span><span class="p">[</span><span class="nv">k</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">v</span>
<span class="linenos">21</span><span class="w"> </span><span class="kr">end</span>
<span class="linenos">22</span><span class="w"> </span><span class="kr">end</span>
<span class="linenos">23</span>
<span class="linenos">24</span><span class="w"> </span><span class="nv">proto</span><span class="p">.</span><span class="py">__index</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">proto</span>
<span class="linenos">25</span><span class="w"> </span><span class="nv">proto</span><span class="p">.</span><span class="py">__super</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">self</span>
<span class="linenos">26</span>
<span class="linenos">27</span><span class="w"> </span><span class="kr">return</span><span class="w"> </span><span class="nb">setmetatable</span><span class="p">(</span><span class="nv">proto</span><span class="p">,</span><span class="w"> </span><span class="nv">self</span><span class="p">)</span>
<span class="linenos">28</span><span class="kr">end</span>
</code></pre></div>
<p>How does this work? What does this mean? What <em>is</em> a prototype chain, anyway? Dearest reader: it extremely does not matter. No one cares. I would have to stare at this for ten minutes to even begin to explain it. Every line is oozing with subtlety. To be honest, even though I describe this series as “from scratch”, this is one of the very few things that I copy/pasted wholesale from an earlier game. I know this does the bare minimum I need and I absolutely do not want to waste time reinventing it incorrectly. To drive that point home: I wrote <em>collision detection</em> from scratch, but I <em>copy/pasted this</em>. (But if you really want to know, I’ll explain it in an appendix.)</p>
<p>Feel free to copy/paste mine, if you like. You can also find a number of tiny Lua object systems floating around online, but with tokens at a premium, I wanted something <em>microscopic</em>. This basically does constructors, inheritance, and nothing else.</p>
<p>(Oh, I don’t think I mentioned, but the <code>--</code> prefix indicates a Lua <em>comment</em>. Comments are ignored by the computer and tend to contain notes that are helpful for humans to follow. They don’t count against the <span class="caps">PICO</span>-8 token limit, but they <em>do</em> count against the total size limit, alas.)</p>
<p>The upshot is that I can now write stuff like this:</p>
<div class="highlight"><pre><span></span><code><span class="linenos"> 1</span><span class="kd">local</span><span class="w"> </span><span class="nv">vec</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">obj</span><span class="p">:</span><span class="py">extend</span><span class="p">{}</span>
<span class="linenos"> 2</span>
<span class="linenos"> 3</span><span class="kr">function</span><span class="w"> </span><span class="nc">vec</span><span class="p">:</span><span class="nf">init</span><span class="p">(</span><span class="nv">x</span><span class="p">,</span><span class="w"> </span><span class="nv">y</span><span class="p">)</span>
<span class="linenos"> 4</span><span class="w"> </span><span class="nv">self</span><span class="p">.</span><span class="py">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">x</span><span class="w"> </span><span class="ow">or</span><span class="w"> </span><span class="mi">0</span>
<span class="linenos"> 5</span><span class="w"> </span><span class="nv">self</span><span class="p">.</span><span class="py">y</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">y</span><span class="w"> </span><span class="ow">or</span><span class="w"> </span><span class="mi">0</span>
<span class="linenos"> 6</span><span class="kr">end</span>
<span class="linenos"> 7</span>
<span class="linenos"> 8</span><span class="kr">function</span><span class="w"> </span><span class="nc">vec</span><span class="p">:</span><span class="nf">__add</span><span class="p">(</span><span class="nv">v</span><span class="p">)</span>
<span class="linenos"> 9</span><span class="w"> </span><span class="kr">return</span><span class="w"> </span><span class="nf">vec</span><span class="p">(</span><span class="nv">self</span><span class="p">.</span><span class="py">x</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nv">v</span><span class="p">.</span><span class="py">x</span><span class="p">,</span><span class="w"> </span><span class="nv">self</span><span class="p">.</span><span class="py">y</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nv">v</span><span class="p">.</span><span class="py">y</span><span class="p">)</span>
<span class="linenos">10</span><span class="kr">end</span>
<span class="linenos">11</span>
<span class="linenos">12</span><span class="kr">function</span><span class="w"> </span><span class="nc">vec</span><span class="p">:</span><span class="nf">__sub</span><span class="p">(</span><span class="nv">v</span><span class="p">)</span>
<span class="linenos">13</span><span class="w"> </span><span class="kr">return</span><span class="w"> </span><span class="nf">vec</span><span class="p">(</span><span class="nv">self</span><span class="p">.</span><span class="py">x</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nv">v</span><span class="p">.</span><span class="py">x</span><span class="p">,</span><span class="w"> </span><span class="nv">self</span><span class="p">.</span><span class="py">y</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nv">v</span><span class="p">.</span><span class="py">y</span><span class="p">)</span>
<span class="linenos">14</span><span class="kr">end</span>
<span class="linenos">15</span>
<span class="linenos">16</span><span class="kr">function</span><span class="w"> </span><span class="nc">vec</span><span class="p">:</span><span class="nf">iadd</span><span class="p">(</span><span class="nv">v</span><span class="p">)</span>
<span class="linenos">17</span><span class="w"> </span><span class="nv">self</span><span class="p">.</span><span class="py">x</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="nv">v</span><span class="p">.</span><span class="py">x</span>
<span class="linenos">18</span><span class="w"> </span><span class="nv">self</span><span class="p">.</span><span class="py">y</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="nv">v</span><span class="p">.</span><span class="py">y</span>
<span class="linenos">19</span><span class="kr">end</span>
</code></pre></div>
<p>This creates a… well, terminology is tricky, but I’ll call it a <em>type</em> while doing air-quotes and glancing behind me to see if any Haskell programmers are listening. (It’s not much like the notion of a type in many other languages, but it’s the closest I’m going to get.) Now I can combine an x- and y-coordinate together as a single object, a single <em>thing</em>, without having to juggle them separately. I’m calling that kind of thing a <code>vec</code>, short for <em>vector</em>, the name mathematicians give to a set of coordinates. (More or less. That’s not quite right, but don’t worry about it yet.)</p>
<aside class="aside--computers-are-bad">
<p>"Vector" is also the name C++ and Rust programmers give to a <em>resizeable</em> list of things. I told you, we are awful at naming.</p>
</aside>
<aside class="aside--look-out">
<p>Usually, I'd want to use so-called CamelCase names for types (so <code>Vec</code> or <code>Vector</code> or <code>Vector2</code>) and lowercase names for values (so <code>vec</code> would be <em>a particular vector</em>). Unfortunately, the PICO-8 editor doesn't do capital letters — or maybe only does capital letters — so I just have to be careful. The total amount of code I can write is pretty limited, so I won't have too many types anyway, and hopefully I can remember which names are one of the handful of types I've defined.</p>
</aside>
<p>After the above incantation, I can create <em>a</em> <code>vec</code> by calling it like a function. Note that the arguments ultimately arrive in <code>vec:init</code>, loosely called a <em>constructor</em>, which stores them in <code>self.x</code> and <code>self.y</code> — where <code>self</code> is the <code>vec</code> being created.</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="c1">-- this is example code, not part of the game</span>
<span class="linenos">2</span><span class="kd">local</span><span class="w"> </span><span class="nv">a</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">vec</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">)</span>
<span class="linenos">3</span><span class="nb">print</span><span class="p">(</span><span class="s2">"x = "</span><span class="p">,</span><span class="w"> </span><span class="nv">a</span><span class="p">.</span><span class="py">x</span><span class="p">,</span><span class="w"> </span><span class="s2">" y = "</span><span class="p">,</span><span class="w"> </span><span class="nv">a</span><span class="p">.</span><span class="py">y</span><span class="p">)</span><span class="w"> </span><span class="c1">-- x = 1 y = 2</span>
</code></pre></div>
<p>That <code>iadd</code> thing is a <em>method</em>, a special function that I can call <em>on</em> a <code>vec</code>. It’s like every <code>vec</code> carries around its own little bag of functions anywhere it appears — and since they’re specific to <code>vec</code>, I don’t have to worry about reusing names. (In fact, reusing names can be very helpful, as we’ll see later!)</p>
<p>The name <code>iadd</code> is (very!) short for “in-place add”, suggesting that the first vector adds the second vector <em>to itself</em> rather than creating a new third vector. That’s something I expect to be doing a lot, and making a method for it saves me some precious tokens.</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="c1">-- example code</span>
<span class="linenos">2</span><span class="kd">local</span><span class="w"> </span><span class="nv">v</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">vec</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">)</span>
<span class="linenos">3</span><span class="kd">local</span><span class="w"> </span><span class="nv">w</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">vec</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span><span class="w"> </span><span class="mi">4</span><span class="p">)</span>
<span class="linenos">4</span><span class="nv">v</span><span class="p">:</span><span class="nf">iadd</span><span class="p">(</span><span class="nv">w</span><span class="p">)</span>
<span class="linenos">5</span><span class="nb">print</span><span class="p">(</span><span class="s2">"x = "</span><span class="p">,</span><span class="w"> </span><span class="nv">v</span><span class="p">.</span><span class="py">x</span><span class="p">,</span><span class="w"> </span><span class="s2">" y = "</span><span class="p">,</span><span class="w"> </span><span class="nv">v</span><span class="p">.</span><span class="py">y</span><span class="p">)</span><span class="w"> </span><span class="c1">-- x = 4 y = 6</span>
</code></pre></div>
<aside class="aside--look-out">
<p>Methods in Lua are called with a <strong>colon</strong>! If you write <code>v.iadd(w)</code> instead, either you'll get an extremely cryptic error or something very wrong will happen. Sorry; this is one of Lua's subtle pitfalls and there's not really any good way to prevent it.</p>
</aside>
<p>Finally, those funny <code>__add</code> and <code>__sub</code> methods are special to Lua (if enchanted correctly, which is part of what the <code>obj</code> gobbledygook does) — they let me use <code>+</code> and <code>-</code> on my <code>vec</code>s just like they were numbers.</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="c1">-- example code</span>
<span class="linenos">2</span><span class="kd">local</span><span class="w"> </span><span class="nv">q</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">vec</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">)</span>
<span class="linenos">3</span><span class="kd">local</span><span class="w"> </span><span class="nv">r</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">vec</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span><span class="w"> </span><span class="mi">4</span><span class="p">)</span>
<span class="linenos">4</span><span class="kd">local</span><span class="w"> </span><span class="nv">s</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">q</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nv">r</span>
<span class="linenos">5</span><span class="nb">print</span><span class="p">(</span><span class="s2">"x = "</span><span class="p">,</span><span class="w"> </span><span class="nv">s</span><span class="p">.</span><span class="py">x</span><span class="p">,</span><span class="w"> </span><span class="s2">" y = "</span><span class="p">,</span><span class="w"> </span><span class="nv">s</span><span class="p">.</span><span class="py">y</span><span class="p">)</span><span class="w"> </span><span class="c1">-- x = 4 y = 6</span>
</code></pre></div>
<aside class="aside--note-from-future">
<p>I find myself wondering why I made an <code>iadd</code> method when I could have just used <code>+=</code>. The final game only even uses <code>iadd</code> four times, and it's more tokens than <code>+=</code>!</p>
</aside>
<p>This is the core idea of objects. A <code>vec</code> has some data — <code>x</code> and <code>y</code> — and some code — for adding another <code>vec</code> to itself. If I later discover some new thing I want a <code>vec</code> to be able to do, I can add another method here, and it’ll be available on every <code>vec</code> throughout my game. I can repeat myself a little bit less, <em>and</em> I can keep these related ideas together, separate from everything else.</p>
<p>Get the basic jist? I hope so, because I’ve really gotta get a move on here.</p>
<h2 id="objectifying-star-anise"><a class="toclink" href="proxy.php?url=#objectifying-star-anise">Objectifying Star Anise</a></h2>
<p>Now that I have a way to define objects, I can turn Star Anise into one.</p>
<div class="highlight"><pre><span></span><code><span class="linenos"> 1</span><span class="kr">function</span><span class="w"> </span><span class="nf">b2n</span><span class="p">(</span><span class="nv">b</span><span class="p">)</span>
<span class="linenos"> 2</span><span class="w"> </span><span class="kr">return</span><span class="w"> </span><span class="nv">b</span><span class="w"> </span><span class="ow">and</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="ow">or</span><span class="w"> </span><span class="mi">0</span>
<span class="linenos"> 3</span><span class="kr">end</span>
<span class="linenos"> 4</span>
<span class="linenos"> 5</span><span class="kd">local</span><span class="w"> </span><span class="nv">t</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span>
<span class="linenos"> 6</span><span class="kd">local</span><span class="w"> </span><span class="nv">player</span>
<span class="linenos"> 7</span>
<span class="linenos"> 8</span><span class="kd">local</span><span class="w"> </span><span class="nv">anise_stand</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="mi">17</span><span class="p">,</span><span class="w"> </span><span class="mi">18</span><span class="p">,</span><span class="w"> </span><span class="mi">33</span><span class="p">,</span><span class="w"> </span><span class="mi">34</span><span class="p">}</span>
<span class="linenos"> 9</span><span class="kd">local</span><span class="w"> </span><span class="nv">anise_jump</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="mi">3</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="mi">17</span><span class="p">,</span><span class="w"> </span><span class="mi">18</span><span class="p">,</span><span class="w"> </span><span class="mi">19</span><span class="p">,</span><span class="w"> </span><span class="mi">35</span><span class="p">}</span>
<span class="linenos">10</span><span class="kd">local</span><span class="w"> </span><span class="nv">anise</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">obj</span><span class="p">:</span><span class="py">extend</span><span class="p">{</span>
<span class="linenos">11</span><span class="w"> </span><span class="nv">move</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">vec</span><span class="p">(),</span>
<span class="linenos">12</span><span class="w"> </span><span class="nv">left</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span>
<span class="linenos">13</span><span class="p">}</span>
<span class="linenos">14</span>
<span class="linenos">15</span><span class="kr">function</span><span class="w"> </span><span class="nc">anise</span><span class="p">:</span><span class="nf">init</span><span class="p">(</span><span class="nv">pos</span><span class="p">)</span>
<span class="linenos">16</span><span class="w"> </span><span class="nv">self</span><span class="p">.</span><span class="py">pos</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">pos</span>
<span class="linenos">17</span><span class="kr">end</span>
<span class="linenos">18</span>
<span class="linenos">19</span><span class="kr">function</span><span class="w"> </span><span class="nc">anise</span><span class="p">:</span><span class="nf">update</span><span class="p">()</span>
<span class="linenos">20</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">self</span><span class="p">.</span><span class="py">move</span><span class="p">.</span><span class="py">x</span><span class="w"> </span><span class="o">></span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="kr">then</span>
<span class="linenos">21</span><span class="w"> </span><span class="nv">self</span><span class="p">.</span><span class="py">left</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">false</span>
<span class="linenos">22</span><span class="w"> </span><span class="kr">elseif</span><span class="w"> </span><span class="nv">self</span><span class="p">.</span><span class="py">move</span><span class="p">.</span><span class="py">x</span><span class="w"> </span><span class="o"><</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="kr">then</span>
<span class="linenos">23</span><span class="w"> </span><span class="nv">self</span><span class="p">.</span><span class="py">left</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">true</span>
<span class="linenos">24</span><span class="w"> </span><span class="kr">end</span>
<span class="linenos">25</span>
<span class="linenos">26</span><span class="w"> </span><span class="nv">self</span><span class="p">.</span><span class="py">pos</span><span class="p">:</span><span class="nf">iadd</span><span class="p">(</span><span class="nv">self</span><span class="p">.</span><span class="py">move</span><span class="p">)</span>
<span class="linenos">27</span><span class="kr">end</span>
<span class="linenos">28</span>
<span class="linenos">29</span><span class="kr">function</span><span class="w"> </span><span class="nc">anise</span><span class="p">:</span><span class="nf">draw</span><span class="p">()</span>
<span class="linenos">30</span><span class="w"> </span><span class="kd">local</span><span class="w"> </span><span class="nv">pose</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">anise_stand</span>
<span class="linenos">31</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">self</span><span class="p">.</span><span class="py">move</span><span class="p">.</span><span class="py">x</span><span class="w"> </span><span class="o">~=</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="ow">or</span><span class="w"> </span><span class="nv">self</span><span class="p">.</span><span class="py">move</span><span class="p">.</span><span class="py">y</span><span class="w"> </span><span class="o">~=</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w"> </span><span class="ow">and</span><span class="w"> </span><span class="nv">t</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="mi">8</span><span class="w"> </span><span class="o"><</span><span class="w"> </span><span class="mi">4</span><span class="w"> </span><span class="kr">then</span>
<span class="linenos">32</span><span class="w"> </span><span class="nv">pose</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">anise_jump</span>
<span class="linenos">33</span><span class="w"> </span><span class="kr">end</span>
<span class="linenos">34</span><span class="w"> </span><span class="kd">local</span><span class="w"> </span><span class="nv">y</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">self</span><span class="p">.</span><span class="py">pos</span><span class="p">.</span><span class="py">y</span>
<span class="linenos">35</span><span class="w"> </span><span class="kd">local</span><span class="w"> </span><span class="nv">x0</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">self</span><span class="p">.</span><span class="py">pos</span><span class="p">.</span><span class="py">x</span>
<span class="linenos">36</span><span class="w"> </span><span class="kd">local</span><span class="w"> </span><span class="nv">dx</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">8</span>
<span class="linenos">37</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">self</span><span class="p">.</span><span class="py">left</span><span class="w"> </span><span class="kr">then</span>
<span class="linenos">38</span><span class="w"> </span><span class="nv">dx</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">-</span><span class="mi">8</span>
<span class="linenos">39</span><span class="w"> </span><span class="nv">x0</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="mi">8</span>
<span class="linenos">40</span><span class="w"> </span><span class="kr">end</span>
<span class="linenos">41</span><span class="w"> </span><span class="kd">local</span><span class="w"> </span><span class="nv">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">x0</span>
<span class="linenos">42</span><span class="w"> </span><span class="kr">for</span><span class="w"> </span><span class="nv">i</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="o">#</span><span class="nv">pose</span><span class="w"> </span><span class="kr">do</span>
<span class="linenos">43</span><span class="w"> </span><span class="nf">spr</span><span class="p">(</span><span class="nv">pose</span><span class="p">[</span><span class="nv">i</span><span class="p">],</span><span class="w"> </span><span class="nv">x</span><span class="p">,</span><span class="w"> </span><span class="nv">y</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="nv">self</span><span class="p">.</span><span class="py">left</span><span class="p">)</span>
<span class="linenos">44</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">i</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="kr">then</span>
<span class="linenos">45</span><span class="w"> </span><span class="nv">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">x0</span>
<span class="linenos">46</span><span class="w"> </span><span class="nv">y</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="mi">8</span>
<span class="linenos">47</span><span class="w"> </span><span class="kr">else</span>
<span class="linenos">48</span><span class="w"> </span><span class="nv">x</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="nv">dx</span>
<span class="linenos">49</span><span class="w"> </span><span class="kr">end</span>
<span class="linenos">50</span><span class="w"> </span><span class="kr">end</span>
<span class="linenos">51</span><span class="kr">end</span>
<span class="linenos">52</span>
<span class="linenos">53</span><span class="kr">function</span><span class="w"> </span><span class="nf">_init</span><span class="p">()</span>
<span class="linenos">54</span><span class="w"> </span><span class="nv">player</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">anise</span><span class="p">(</span><span class="nf">vec</span><span class="p">(</span><span class="mi">64</span><span class="p">,</span><span class="w"> </span><span class="mi">64</span><span class="p">))</span>
<span class="linenos">55</span><span class="kr">end</span>
<span class="linenos">56</span>
<span class="linenos">57</span><span class="kr">function</span><span class="w"> </span><span class="nf">_update</span><span class="p">()</span>
<span class="linenos">58</span><span class="w"> </span><span class="nv">t</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="mi">1</span>
<span class="linenos">59</span><span class="w"> </span><span class="nv">t</span><span class="w"> </span><span class="o">%=</span><span class="w"> </span><span class="mi">120</span>
<span class="linenos">60</span><span class="w"> </span><span class="nv">player</span><span class="p">.</span><span class="py">move</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">vec</span><span class="p">(</span>
<span class="linenos">61</span><span class="w"> </span><span class="nf">b2n</span><span class="p">(</span><span class="nf">btn</span><span class="p">(</span><span class="err">➡️</span><span class="p">))</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nf">b2n</span><span class="p">(</span><span class="nf">btn</span><span class="p">(</span><span class="err">⬅️</span><span class="p">)),</span>
<span class="linenos">62</span><span class="w"> </span><span class="nf">b2n</span><span class="p">(</span><span class="nf">btn</span><span class="p">(</span><span class="err">⬇️</span><span class="p">))</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nf">b2n</span><span class="p">(</span><span class="nf">btn</span><span class="p">(</span><span class="err">⬆️</span><span class="p">)))</span>
<span class="linenos">63</span><span class="w"> </span><span class="nv">player</span><span class="p">:</span><span class="nf">update</span><span class="p">()</span>
<span class="linenos">64</span><span class="kr">end</span>
<span class="linenos">65</span>
<span class="linenos">66</span><span class="kr">function</span><span class="w"> </span><span class="nf">_draw</span><span class="p">()</span>
<span class="linenos">67</span><span class="w"> </span><span class="nf">cls</span><span class="p">()</span>
<span class="linenos">68</span><span class="w"> </span><span class="nv">player</span><span class="p">:</span><span class="nf">draw</span><span class="p">()</span>
<span class="linenos">69</span><span class="kr">end</span>
</code></pre></div>
<p>What a mouthful! But <em>for the most part</em>, this is the same code as before, just rearranged. For example, the new <code>anise:draw()</code> method has basically been cut and pasted from my old <code>_draw()</code> — all except the <code>cls()</code> call, since that has nothing to do with drawing Star Anise.</p>
<p>I’ve combined the <code>px</code> and <code>py</code> variables into a single vector, <code>pos</code> (short for “position”), which I now have to refer to as <code>self.pos</code> — that’s so <span class="caps">PICO</span>-8 knows whose <code>pos</code> I’m talking about. After all, it’s theoretically possible for me to create more than one Star Anise now. I won’t, but <span class="caps">PICO</span>-8 doesn’t know that!</p>
<aside class="aside--tricky-tradeoff">
<p>A downside of this approach is that it costs more tokens — <code>px</code> is one token, but <code>self.pos.x</code> is three (and a few more bytes, too). With any luck, this extra cost in code size will be balanced out later when I find ways to reuse some of this code. Some PICO-8 games deliberately sacrifice structure to save tokens.</p>
</aside>
<p>A Star Anise object is created and assigned to <code>player</code> when the game starts, and then <code>_update()</code> calls <code>player:update()</code> and <code>_draw()</code> calls <code>player:draw()</code> to get the same effects as before.</p>
<p>I did make one moderately dramatic change in this code. The wordy code I had for reading buttons has become much more compact and inscrutable, and the <code>moving</code> variable is gone. A big part of the reason for this is that I consider Star Anise’s <em>movement</em> to be part of himself, but reading input to be part of the <em>game</em>, so I wanted to split them up. That means <code>moving</code> is a bit awkward, since I previously updated it as part of reading input. Instead, I’ve turned Star Anise’s movement into another vector, which I set in <code>_update()</code> using this mouthful:</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="c1">-- top-level</span>
<span class="linenos">2</span><span class="kr">function</span><span class="w"> </span><span class="nf">b2n</span><span class="p">(</span><span class="nv">b</span><span class="p">)</span>
<span class="linenos">3</span><span class="w"> </span><span class="kr">return</span><span class="w"> </span><span class="nv">b</span><span class="w"> </span><span class="ow">and</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="ow">or</span><span class="w"> </span><span class="mi">0</span>
<span class="linenos">4</span><span class="kr">end</span>
<span class="linenos">5</span>
<span class="linenos">6</span><span class="c1">-- in _update()</span>
<span class="linenos">7</span><span class="w"> </span><span class="nv">player</span><span class="p">.</span><span class="py">move</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">vec</span><span class="p">(</span>
<span class="linenos">8</span><span class="w"> </span><span class="nf">b2n</span><span class="p">(</span><span class="nf">btn</span><span class="p">(</span><span class="err">➡️</span><span class="p">))</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nf">b2n</span><span class="p">(</span><span class="nf">btn</span><span class="p">(</span><span class="err">⬅️</span><span class="p">)),</span>
<span class="linenos">9</span><span class="w"> </span><span class="nf">b2n</span><span class="p">(</span><span class="nf">btn</span><span class="p">(</span><span class="err">⬇️</span><span class="p">))</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nf">b2n</span><span class="p">(</span><span class="nf">btn</span><span class="p">(</span><span class="err">⬆️</span><span class="p">)))</span>
</code></pre></div>
<p>The <code>b2n()</code> function turns a <strong>b</strong>utton into a <strong>n</strong>umber, and I only use it here. It turns <code>true</code> into 1 and <code>false</code> into 0. Think of it as measuring “how much” the button is held down, from 0 to 1, except of course there can’t be any answer in the middle.</p>
<aside class="aside--look-out">
<p>In Lua, <code>x = a and b or c</code> is <em>kind of</em> like a very terse way of writing:</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="kr">if</span><span class="w"> </span><span class="nv">a</span><span class="w"> </span><span class="kr">then</span>
<span class="linenos">2</span><span class="w"> </span><span class="nv">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">b</span>
<span class="linenos">3</span><span class="kr">else</span>
<span class="linenos">4</span><span class="w"> </span><span class="nv">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">c</span>
<span class="linenos">5</span><span class="kr">end</span>
</code></pre></div>
<p>It's a clever abuse of <code>and</code> and <code>or</code> that lets you express something like an <code>if</code>, except producing a value instead of executing code. It also has a serious pitfall: if <code>b</code> is a falsy value (so, <code>false</code> or <code>nul</code>), the result will <em>always</em> be <code>c</code>! For this reason, I <strong>strongly</strong> recommend against doing this (in <em>any</em> language) — I just really, really wanted <code>b2n()</code> to take up as few tokens as possible.</p>
</aside>
<p>Unpacking that a bit further, <code>b2n(btn(➡️)) - b2n(btn(⬅️))</code> means “how much we’re holding right, minus how much we’re holding left”. If the player is only holding the right button, that’s 1 - 0 = 1. If they’re only holding the left button, that’s 0 - 1 = -1. If they’re holding both or neither, that’s 0. The results are the same as before, but the code is smaller.</p>
<aside class="aside--note-from-future">
<p>This is unreadable garbage that barely saves any tokens, and I will eventually succumb to shame and scrap it. <em>Especially</em> since I don't need vertical movement!</p>
</aside>
<p>Once Star Anise’s <code>move</code> is set, the rest works similarly to before: I update <code>left</code> based on horizontal movement (but leave it alone when there isn’t anyway), I alter his position (now using <code>:iadd()</code>), and I use the walk animation when he’s moving at all. And that’s it!</p>
<h2 id="from-one-to-many"><a class="toclink" href="proxy.php?url=#from-one-to-many">From one to many</a></h2>
<p>I like to use the term “actor” to refer to a distinct <em>thing</em> in the game world; it conjures a charming and concrete image of various characters performing on a stage. I think I picked it up from the Doom source code. “Entity” is more common and is used heavily in Unity, but can be confused with an “entity–component–system” setup, which Unity <em>also</em> supports. And then there are heretics who refer to game things as “objects” even though that’s also a programming term.</p>
<p>This code is a fine start, but it’s not quite what I want. There’s nothing here actually called an actor, for starters. My setup still only works for Star Anise!</p>
<p>I’d better fix that. The notion of an “actor” is pretty vague, so a generic actor won’t do much by itself, but it’s nice to define one as a template for how I expect real actors to work.</p>
<div class="highlight"><pre><span></span><code><span class="linenos"> 1</span><span class="kd">local</span><span class="w"> </span><span class="nv">actor</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">obj</span><span class="p">:</span><span class="py">extend</span><span class="p">{}</span>
<span class="linenos"> 2</span>
<span class="linenos"> 3</span><span class="kr">function</span><span class="w"> </span><span class="nc">actor</span><span class="p">:</span><span class="nf">init</span><span class="p">(</span><span class="nv">pos</span><span class="p">)</span>
<span class="linenos"> 4</span><span class="w"> </span><span class="nv">self</span><span class="p">.</span><span class="py">pos</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">pos</span>
<span class="linenos"> 5</span><span class="kr">end</span>
<span class="linenos"> 6</span>
<span class="linenos"> 7</span><span class="kr">function</span><span class="w"> </span><span class="nc">actor</span><span class="p">:</span><span class="nf">update</span><span class="p">()</span>
<span class="linenos"> 8</span><span class="kr">end</span>
<span class="linenos"> 9</span>
<span class="linenos">10</span><span class="kr">function</span><span class="w"> </span><span class="nc">actor</span><span class="p">:</span><span class="nf">draw</span><span class="p">()</span>
<span class="linenos">11</span><span class="kr">end</span>
</code></pre></div>
<p>How does a blank actor update or draw itself? By doing nothing.</p>
<p>(I do assume that every actor has a position; this may not necessarily be the case in games with very broad ideas about what an “actor” is, but it’s reasonable enough for my purposes.)</p>
<p>Now, to link this with Star Anise, I’ll have <code>anise</code> <em>inherit</em> from <code>actor</code>. That means he’ll become a specialized kind of <code>actor</code>, and in particular, all the methods on <code>actor</code> will also appear on <code>anise</code>. You may notice that <code>anise</code> was previously a specialized kind of <code>obj</code> (like <code>actor</code> and <code>vec</code>) — in fact, the only reason I can call <code>vec(x, y)</code> like a function is that it inherits some magic stuff from <code>obj</code>. Surprise!</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="kd">local</span><span class="w"> </span><span class="nv">anise</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">actor</span><span class="p">:</span><span class="py">extend</span><span class="p">{</span>
</code></pre></div>
<p>I can now delete <code>anise:init()</code>, since it’s identical to <code>actor:init()</code>. I still have <code>anise:update()</code> and <code>anise:draw()</code>, which override the methods on <code>actor</code>, so those don’t need changing.</p>
<p>Everything <em>still</em> only works for Star Anise, but I’m getting closer! I only need one more change. Instead of having only <code>player</code>, I will make a <em>list</em> of actors.</p>
<aside class="aside--well-actually">
<p>The Lua structure I'm using here is called a <em>table</em>; strictly speaking, there's nothing in Lua called a <em>list</em>. But tables are used multiple ways in Lua, so I'm going to call one a list when it's intended as... well, as a list of things. Intent is important!</p>
<p>Oh, and some programmers might be confused about "list" because they assume it means a <em>linked list</em>, which is a very different thing entirely, and they would call this an <em>array</em>. Or maybe even a <em>vector</em>. I told you, we are really quite bad at naming things.</p>
</aside>
<div class="highlight"><pre><span></span><code><span class="linenos"> 1</span><span class="c1">-- at the top</span>
<span class="linenos"> 2</span><span class="kd">local</span><span class="w"> </span><span class="nv">actors</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{}</span>
<span class="linenos"> 3</span>
<span class="linenos"> 4</span><span class="kr">function</span><span class="w"> </span><span class="nf">_init</span><span class="p">()</span>
<span class="linenos"> 5</span><span class="w"> </span><span class="nv">player</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">anise</span><span class="p">(</span><span class="nf">vec</span><span class="p">(</span><span class="mi">64</span><span class="p">,</span><span class="w"> </span><span class="mi">64</span><span class="p">))</span>
<span class="linenos"> 6</span><span class="w"> </span><span class="nf">add</span><span class="p">(</span><span class="nv">actors</span><span class="p">,</span><span class="w"> </span><span class="nv">player</span><span class="p">)</span>
<span class="linenos"> 7</span><span class="kr">end</span>
<span class="linenos"> 8</span>
<span class="linenos"> 9</span><span class="kr">function</span><span class="w"> </span><span class="nf">_update</span><span class="p">()</span>
<span class="linenos">10</span><span class="w"> </span><span class="c1">-- ...mostly same as before...</span>
<span class="linenos">11</span><span class="w"> </span><span class="kr">for</span><span class="w"> </span><span class="nv">actor</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nf">all</span><span class="p">(</span><span class="nv">actors</span><span class="p">)</span><span class="w"> </span><span class="kr">do</span>
<span class="linenos">12</span><span class="w"> </span><span class="nv">actor</span><span class="p">:</span><span class="nf">update</span><span class="p">()</span>
<span class="linenos">13</span><span class="w"> </span><span class="kr">end</span>
<span class="linenos">14</span><span class="kr">end</span>
<span class="linenos">15</span>
<span class="linenos">16</span><span class="kr">function</span><span class="w"> </span><span class="nf">_draw</span><span class="p">()</span>
<span class="linenos">17</span><span class="w"> </span><span class="nf">cls</span><span class="p">()</span>
<span class="linenos">18</span><span class="w"> </span><span class="kr">for</span><span class="w"> </span><span class="nv">actor</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nf">all</span><span class="p">(</span><span class="nv">actors</span><span class="p">)</span><span class="w"> </span><span class="kr">do</span>
<span class="linenos">19</span><span class="w"> </span><span class="nv">actor</span><span class="p">:</span><span class="nf">draw</span><span class="p">()</span>
<span class="linenos">20</span><span class="w"> </span><span class="kr">end</span>
<span class="linenos">21</span><span class="kr">end</span>
</code></pre></div>
<p>This does pretty much what it reads like. The <code>add()</code> function, specific to <span class="caps">PICO</span>-8, adds an item to the end of a list. The <code>all()</code> function, also specific to <span class="caps">PICO</span>-8, helps go through a list. And the <code>for</code> blocks mean, for each thing in this list, run this code.</p>
<p>Now, at last, I have something that could work for actors other than Star Anise. All I need to do is define them and add them to the <code>actors</code> list, and they’ll automatically be updated and drawn, just like him!</p>
<aside class="aside--look-out">
<p>I have done a slightly naughty thing here. I used <code>actor</code> as the name of a <em>type</em>, a generic actor with no particular behavior, but I also used it in those <code>for</code> loops as the name of a <em>specific actor</em>. This is generally something to avoid — at best it can confuse a reader, at worst you find yourself wanting to use both things at the same time. Like I said before, I would usually name types like <code>Actor</code>, but the PICO-8 prevents it.</p>
<p>Fear not, though: Lua's <code>for</code> statement makes a <em>new</em> variable called <code>actor</code> that hides the outer one, and then forgets about it at the end of the block, leaving the original safely untouched.</p>
</aside>
<p>Admittedly, this hasn’t gotten me anywhere concrete. The game still plays exactly the same as it did when I started. I’m betting that I’ll eventually have more than one actor, though, so I might as well lay the groundwork for that now while it’s easy. It doesn’t take much effort, and I find that if I give myself little early inroads like this, it feels like less of a slog to later come back and expand on the ideas. This is the sort of thing I meant by more structure revealing itself — once I have <em>one</em> actor, a natural next step is to allow for <em>several</em> actors.</p>
<h2 id="preparing-for-collision-detection"><a class="toclink" href="proxy.php?url=#preparing-for-collision-detection">Preparing for collision detection</a></h2>
<p>I’ve put it off long enough. I can’t avoid it any longer. But it’s complicated enough to deserve its own post, so I don’t quite want to do it yet.</p>
<p>Instead, I’ll write as much code as possible <em>except for</em> the actual collision detection. There’s a bit more work to do to plug it in.</p>
<p>For example: what am I going to collide <em>with</em>? The only thing in the universe, currently, is Star Anise himself. It would be nice to have, say, some ground. And that’s a great excuse to toodle around a bit in the sprite editor.</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/gamedev-from-scratch/v1-ground.png" alt="A set of simple ground tiles, drawn in the PICO-8 sprite editor">
</div>
<p>I went through several iterations before landing on this. Star Anise lives on a moon, so that was my guiding principle. The moon is gray and dusty and pitted, so at first I tried drawing a tile with tiny craters in it. Unfortunately, that was a busy mess to look at when tiled, and I didn’t think I’d have enough tile space for having different variants of tiles. I’m already using 9 tiles here just to have neat edges.</p>
<p>And so I landed on this simple pattern with just enough texture to be reminiscent of <em>something</em>, which is all you really need with low-res sprite art. It worked out well enough to survive, nearly unchanged, all the way to the final game. It was inspired by a vague memory of Starbound’s <a href="proxy.php?url=https://starbounder.org/Moondust">moondust</a> tiles, which I was pretty sure had diagonal striping, though I didn’t actually look at them to be sure.</p>
<p>You may notice I drew these on the second tab of sprites. I want to be able to find tiles quickly when drawing maps, so I thought I’d put “terrain” on a dedicated tab and reserve the first one for Star Anise, other actors, special effects, and other less-common tiles. That turned out to be a good idea.</p>
<p>You may <em>also</em> notice that one of those dots on the middle right is lit up. How mysterious! We’ll get to that next time.</p>
<p>With a few simple tiles drawn, I can sprinkle a couple in the map tab. I know I want Metroid-style discrete screens, so I’m not worried about camera scrolling yet; the top-left corner (16×16 tiles) is enough to play with for now.</p>
<p>I draw two rows of tiles at the bottom of that screen. It’s a little hard to gauge since the toolbar and status bar get in the way, but the bottom row of the screen will be at y = 15. You can also hold <kbd>Spacebar</kbd> to get a grid, with squares indicating every half-screen.</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/gamedev-from-scratch/v1-map.png" alt="PICO-8's map editor, showing two rows of moon tiles">
</div>
<p>Finally, to make this appear in the game, I need only ask <span class="caps">PICO</span>-8 to draw the map before I draw actors on top of it.</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="kr">function</span><span class="w"> </span><span class="nf">_draw</span><span class="p">()</span>
<span class="linenos">2</span><span class="w"> </span><span class="nf">cls</span><span class="p">()</span>
<span class="linenos">3</span><span class="w"> </span><span class="nf">map</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">32</span><span class="p">,</span><span class="w"> </span><span class="mi">32</span><span class="p">)</span>
<span class="linenos">4</span><span class="w"> </span><span class="kr">for</span><span class="w"> </span><span class="nv">actor</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nf">all</span><span class="p">(</span><span class="nv">actors</span><span class="p">)</span><span class="w"> </span><span class="kr">do</span>
<span class="linenos">5</span><span class="w"> </span><span class="nv">actor</span><span class="p">:</span><span class="nf">draw</span><span class="p">()</span>
<span class="linenos">6</span><span class="w"> </span><span class="kr">end</span>
<span class="linenos">7</span><span class="kr">end</span>
</code></pre></div>
<p>The <span class="caps">PICO</span>-8 <code>map()</code> function takes (at least) six arguments: the top-left corner of the map to start drawing from, measured in tiles; the top-left corner on the screen to draw to, measured in pixels; and the width/height of the rectangle to draw from the map, measured in tiles. This will draw a 32×32 block of tiles from the top-left corner of the map to the top-left corner of the screen.</p>
<aside class="aside--note-from-future">
<p>I'm not sure why I used 32×32 here, when the screen is only 16×16 tiles big!</p>
</aside>
<p>Of course, with no collision detection, those tiles are nothing more than background pixels, and the game treats them as such.</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/gamedev-from-scratch/v1-overlap.png" alt="Star Anise standing in front of the moon tiles">
</div>
<p>No problem. I can fix that. Sort of.</p>
<h2 id="not-quite-collision-detection"><a class="toclink" href="proxy.php?url=#not-quite-collision-detection">Not quite collision detection</a></h2>
<p>I’m not going into collision detection yet, but I can give you a <em>taste</em>, to give you an idea of the goals.</p>
<p>The core of it comes down to this line, from the end of <code>anise:update()</code>.</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="w"> </span><span class="nv">self</span><span class="p">.</span><span class="py">pos</span><span class="p">:</span><span class="nf">iadd</span><span class="p">(</span><span class="nv">self</span><span class="p">.</span><span class="py">move</span><span class="p">)</span>
</code></pre></div>
<p>That moves Star Anise by one pixel in each direction the player is holding. What I want to do is <em>stop him</em> when he hits something solid.</p>
<p>Hm, sounds hard. Let’s think for a moment about a simpler problem: how can I stop him falling through the ground, in the dumbest way possible?</p>
<p>The ground is flat, and it takes up the bottow two rows of tiles. That means its top edge is 14 tiles, or 112 pixels, below the top of the screen. Thus, Star Anise should not be able to move below that line.</p>
<p>But wait! Star Anise’s <em>position</em> is a single point at his top left, not even inside his helmet. What I really want is for his <em>feet</em> to not pass below that line, and the bottom of his feet is three tiles (24 pixels) below his position. Thus, his position should not pass below y = 112 - 24 = 88.</p>
<p>That sounds doable.</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="w"> </span><span class="nv">self</span><span class="p">.</span><span class="py">pos</span><span class="p">:</span><span class="nf">iadd</span><span class="p">(</span><span class="nv">self</span><span class="p">.</span><span class="py">move</span><span class="p">)</span>
<span class="linenos">2</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">self</span><span class="p">.</span><span class="py">pos</span><span class="p">.</span><span class="py">y</span><span class="w"> </span><span class="o">></span><span class="w"> </span><span class="mi">88</span><span class="w"> </span><span class="kr">then</span>
<span class="linenos">3</span><span class="w"> </span><span class="nv">self</span><span class="p">.</span><span class="py">pos</span><span class="p">.</span><span class="py">y</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">88</span>
<span class="linenos">4</span><span class="w"> </span><span class="kr">end</span>
</code></pre></div>
<p>And sure enough, it works!</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/gamedev-from-scratch/v1-collision-taste.gif" alt="Star Anise walking through the air, but not through the floor">
</div>
<p>This isn’t going to get us very far, of course. He still walks through the air, he can still walk off the screen, and if I change the terrain then the code won’t be right any more. I’m also pretty sure I didn’t actually write this in practice. But hopefully it gives you the teeniest idea of the problem we’re going to solve next time.</p>
<p><strong>Part 2: Collision →</strong> (coming soon!)</p>
<h2 id="appendix-the-lua-object-model"><a class="toclink" href="proxy.php?url=#appendix-the-lua-object-model">Appendix: the Lua object model</a></h2>
<p>Really, really, <em>really</em> quickly, here’s how that <code>obj</code> snippet works.</p>
<p>Lua’s primary data structure is the <em>table</em>. It can be used to make ordered lists of things, as I did above with <code>actors</code>, but it can also be used for arbitrary mappings. I can assign some value to a particular <em>key</em>, then quickly look that key up again later. Kind of like a Rolodex.</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="kd">local</span><span class="w"> </span><span class="nv">lunekos</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="linenos">2</span><span class="w"> </span><span class="nv">anise</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"star anise is the best"</span><span class="p">,</span>
<span class="linenos">3</span><span class="w"> </span><span class="nv">purrl</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"purrl is very lovely"</span><span class="p">,</span>
<span class="linenos">4</span><span class="p">}</span>
<span class="linenos">5</span><span class="nb">print</span><span class="p">(</span><span class="nv">lunekos</span><span class="p">[</span><span class="s1">'anise'</span><span class="p">])</span>
</code></pre></div>
<p>Note that the values (and keys!) don’t have to be strings; they can be anything you like, even other tables. But for string keys, you can do something special:</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="nb">print</span><span class="p">(</span><span class="nv">lunekos</span><span class="p">.</span><span class="py">anise</span><span class="p">)</span><span class="w"> </span><span class="c1">-- same as above</span>
</code></pre></div>
<p><em>Everywhere</em> you see a dot (or colon) used in Lua, that’s actually looking up a string in a table.</p>
<p>With me so far? Hope so.</p>
<p>Any Lua table can also be assigned a <em>metatable</em>, which is another table full of various magic stuff that affects the first table’s behavior. Most of the magic stuff takes the form of a special key, starting with two underscores, whose value is a function that will be called in particular circumstances. That function is then called a <em>metamethod</em>. (There’s a <a href="proxy.php?url=https://www.lua.org/pil/13.html">whole section on this in the Lua book</a>, and <a href="proxy.php?url=http://lua-users.org/wiki/MetatableEvents">a summary of metamethods on the Lua wiki</a>.)</p>
<p>One common use for metamethods is to make normal Lua operators work on tables. For example, you can make a table that can be called like a function by providing the <code>__call</code> metamethod.</p>
<div class="highlight"><pre><span></span><code><span class="linenos"> 1</span><span class="kd">local</span><span class="w"> </span><span class="nv">t</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="linenos"> 2</span><span class="w"> </span><span class="nv">stuff</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">5678</span><span class="p">,</span>
<span class="linenos"> 3</span><span class="p">}</span>
<span class="linenos"> 4</span><span class="kd">local</span><span class="w"> </span><span class="nv">meta</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="linenos"> 5</span><span class="w"> </span><span class="c1">-- this is just a regular table key with a function for its value</span>
<span class="linenos"> 6</span><span class="w"> </span><span class="nv">__call</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">function</span><span class="p">(</span><span class="nv">tbl</span><span class="p">)</span>
<span class="linenos"> 7</span><span class="w"> </span><span class="nb">print</span><span class="p">(</span><span class="s2">"my stuff is"</span><span class="p">,</span><span class="w"> </span><span class="nv">tbl</span><span class="p">[</span><span class="s1">'stuff'</span><span class="p">])</span>
<span class="linenos"> 8</span><span class="w"> </span><span class="kr">end</span><span class="p">,</span>
<span class="linenos"> 9</span><span class="p">}</span>
<span class="linenos">10</span><span class="nb">setmetatable</span><span class="p">(</span><span class="nv">t</span><span class="p">,</span><span class="w"> </span><span class="nv">meta</span><span class="p">)</span>
<span class="linenos">11</span><span class="nf">t</span><span class="p">()</span><span class="w"> </span><span class="c1">-- my stuff is 5678</span>
<span class="linenos">12</span><span class="nv">t</span><span class="p">[</span><span class="s1">'stuff'</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"yoinky"</span>
<span class="linenos">13</span><span class="nf">t</span><span class="p">()</span><span class="w"> </span><span class="c1">-- my stuff is yoinky</span>
</code></pre></div>
<p>One especially useful metamethod is <code>__index</code>, which is called when you try to read a key from the table, but the key doesn’t exist.</p>
<div class="highlight"><pre><span></span><code><span class="linenos"> 1</span><span class="kd">local</span><span class="w"> </span><span class="nv">counts</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="linenos"> 2</span><span class="w"> </span><span class="nv">apples</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span>
<span class="linenos"> 3</span><span class="w"> </span><span class="nv">bananas</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span>
<span class="linenos"> 4</span><span class="p">}</span>
<span class="linenos"> 5</span><span class="nb">setmetatable</span><span class="p">(</span><span class="nv">counts</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
<span class="linenos"> 6</span><span class="w"> </span><span class="nv">__index</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">function</span><span class="p">(</span><span class="nv">tbl</span><span class="p">,</span><span class="w"> </span><span class="nv">key</span><span class="p">)</span>
<span class="linenos"> 7</span><span class="w"> </span><span class="kr">return</span><span class="w"> </span><span class="mi">0</span>
<span class="linenos"> 8</span><span class="w"> </span><span class="kr">end</span><span class="p">,</span>
<span class="linenos"> 9</span><span class="p">})</span>
<span class="linenos">10</span><span class="nb">print</span><span class="p">(</span><span class="nv">counts</span><span class="p">.</span><span class="py">bananas</span><span class="p">)</span><span class="w"> </span><span class="c1">-- 3</span>
<span class="linenos">11</span><span class="nb">print</span><span class="p">(</span><span class="nv">counts</span><span class="p">.</span><span class="py">mangoes</span><span class="p">)</span><span class="w"> </span><span class="c1">-- 0</span>
<span class="linenos">12</span><span class="nb">print</span><span class="p">(</span><span class="nv">counts</span><span class="p">.</span><span class="py">apples</span><span class="p">)</span><span class="w"> </span><span class="c1">-- 5</span>
</code></pre></div>
<p>Instead of a function, <code>__index</code> can also be <em>another</em> (third!) table, in which case the key will be looked up in <em>that</em> table instead. And if that table has a metatable with an <code>__index</code>, Lua will follow that too, and keep on going until it gets an answer.</p>
<p>This is essentially what’s called <em>prototypical inheritance</em>, as seen in JavaScript (and more subtly in Python): an object consists of its own values plus a <em>prototype</em>, and if code tries to fetch something from the object that doesn’t exist, the prototype is checked instead. Since the prototype might have its own prototype, the whole sequence is called the <em>prototype chain</em>.</p>
<p>That’s all you need to know to follow the <code>obj</code> snippet, so here it is again.</p>
<div class="highlight"><pre><span></span><code><span class="linenos"> 1</span><span class="kr">function</span><span class="w"> </span><span class="nf">nop</span><span class="p">(...)</span><span class="w"> </span><span class="kr">return</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="kr">end</span>
<span class="linenos"> 2</span>
<span class="linenos"> 3</span><span class="kd">local</span><span class="w"> </span><span class="nv">obj</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="nv">init</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">nop</span><span class="p">}</span>
<span class="linenos"> 4</span><span class="nv">obj</span><span class="p">.</span><span class="py">__index</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">obj</span>
<span class="linenos"> 5</span>
<span class="linenos"> 6</span><span class="kr">function</span><span class="w"> </span><span class="nc">obj</span><span class="p">:</span><span class="nf">__call</span><span class="p">(...)</span>
<span class="linenos"> 7</span><span class="w"> </span><span class="kd">local</span><span class="w"> </span><span class="nv">o</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">setmetatable</span><span class="p">({},</span><span class="w"> </span><span class="nv">self</span><span class="p">)</span>
<span class="linenos"> 8</span><span class="w"> </span><span class="kr">return</span><span class="w"> </span><span class="nv">o</span><span class="p">,</span><span class="w"> </span><span class="nv">o</span><span class="p">:</span><span class="nf">init</span><span class="p">(...)</span>
<span class="linenos"> 9</span><span class="kr">end</span>
<span class="linenos">10</span>
<span class="linenos">11</span><span class="c1">-- subclassing</span>
<span class="linenos">12</span><span class="kr">function</span><span class="w"> </span><span class="nc">obj</span><span class="p">:</span><span class="nf">extend</span><span class="p">(</span><span class="nv">proto</span><span class="p">)</span>
<span class="linenos">13</span><span class="w"> </span><span class="nv">proto</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">proto</span><span class="w"> </span><span class="ow">or</span><span class="w"> </span><span class="p">{}</span>
<span class="linenos">14</span>
<span class="linenos">15</span><span class="w"> </span><span class="c1">-- copy meta values, since lua doesn't walk the prototype chain to find them</span>
<span class="linenos">16</span><span class="w"> </span><span class="kr">for</span><span class="w"> </span><span class="nv">k</span><span class="p">,</span><span class="w"> </span><span class="nv">v</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nb">pairs</span><span class="p">(</span><span class="nv">self</span><span class="p">)</span><span class="w"> </span><span class="kr">do</span>
<span class="linenos">17</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nf">sub</span><span class="p">(</span><span class="nv">k</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s2">"__"</span><span class="w"> </span><span class="kr">then</span>
<span class="linenos">18</span><span class="w"> </span><span class="nv">proto</span><span class="p">[</span><span class="nv">k</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">v</span>
<span class="linenos">19</span><span class="w"> </span><span class="kr">end</span>
<span class="linenos">20</span><span class="w"> </span><span class="kr">end</span>
<span class="linenos">21</span>
<span class="linenos">22</span><span class="w"> </span><span class="nv">proto</span><span class="p">.</span><span class="py">__index</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">proto</span>
<span class="linenos">23</span><span class="w"> </span><span class="nv">proto</span><span class="p">.</span><span class="py">__super</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">self</span>
<span class="linenos">24</span>
<span class="linenos">25</span><span class="w"> </span><span class="kr">return</span><span class="w"> </span><span class="nb">setmetatable</span><span class="p">(</span><span class="nv">proto</span><span class="p">,</span><span class="w"> </span><span class="nv">self</span><span class="p">)</span>
<span class="linenos">26</span><span class="kr">end</span>
</code></pre></div>
<p>The idea is that types are used both as metatables <em>and</em> prototypes — they are always their own <code>__index</code>. At first, we have only <code>obj</code>, which looks like this:</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="kd">local</span><span class="w"> </span><span class="nv">obj</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="linenos">2</span><span class="w"> </span><span class="nv">init</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">nop</span><span class="p">,</span>
<span class="linenos">3</span><span class="w"> </span><span class="nv">__index</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">obj</span><span class="p">,</span>
<span class="linenos">4</span><span class="w"> </span><span class="nv">__call</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">function</span><span class="p">()</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="kr">end</span><span class="p">,</span>
<span class="linenos">5</span><span class="w"> </span><span class="nv">extend</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">function</span><span class="p">()</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="kr">end</span><span class="p">,</span>
<span class="linenos">6</span><span class="p">}</span>
</code></pre></div>
<p>Now we use <code>obj:extend{}</code> to create a new type. Follow along and see what happens. Lua only looks for metamethods like <code>__call</code> directly in the metatable and ignores <code>__index</code>, so I copy them into the new prototype. Then I make the prototype its own <code>__index</code>, as with <code>obj</code>, and also remember the “superclass” as <code>__super</code> (though I never end up using it). Finally I set the “superclass” as the prototype’s metatable.</p>
<p>(Oh, by the way: in Lua, if you call a function with only a single table or string literal as its argument, you can leave off the parentheses. So <code>foo{}</code> just means <code>foo({})</code>.)</p>
<p>That produces something like the following, noting that this is not quite real Lua syntax:</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="kd">local</span><span class="w"> </span><span class="nv">vec</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="linenos">2</span><span class="w"> </span><span class="nv">__index</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">vec</span><span class="p">,</span>
<span class="linenos">3</span><span class="w"> </span><span class="nv">__super</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">obj</span><span class="p">,</span>
<span class="linenos">4</span><span class="w"> </span><span class="nv">__call</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">obj</span><span class="p">.</span><span class="py">__call</span><span class="p">,</span>
<span class="linenos">5</span>
<span class="linenos">6</span><span class="w"> </span><span class="nv">METATABLE</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">obj</span><span class="p">,</span>
<span class="linenos">7</span><span class="p">}</span>
</code></pre></div>
<p>Remember this syntax?</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="kr">function</span><span class="w"> </span><span class="nc">vec</span><span class="p">:</span><span class="nf">init</span><span class="p">(</span><span class="nv">x</span><span class="p">,</span><span class="w"> </span><span class="nv">y</span><span class="p">)</span>
<span class="linenos">2</span><span class="w"> </span><span class="nv">self</span><span class="p">.</span><span class="py">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">x</span><span class="w"> </span><span class="ow">or</span><span class="w"> </span><span class="mi">0</span>
<span class="linenos">3</span><span class="w"> </span><span class="nv">self</span><span class="p">.</span><span class="py">y</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">y</span><span class="w"> </span><span class="ow">or</span><span class="w"> </span><span class="mi">0</span>
<span class="linenos">4</span><span class="kr">end</span>
</code></pre></div>
<p>That is exactly equivalent to:</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="nv">vec</span><span class="p">.</span><span class="py">init</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">function</span><span class="p">(</span><span class="nv">self</span><span class="p">,</span><span class="w"> </span><span class="nv">x</span><span class="p">,</span><span class="w"> </span><span class="nv">y</span><span class="p">)</span>
<span class="linenos">2</span><span class="w"> </span><span class="nv">self</span><span class="p">.</span><span class="py">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">x</span><span class="w"> </span><span class="ow">or</span><span class="w"> </span><span class="mi">0</span>
<span class="linenos">3</span><span class="w"> </span><span class="nv">self</span><span class="p">.</span><span class="py">y</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">y</span><span class="w"> </span><span class="ow">or</span><span class="w"> </span><span class="mi">0</span>
<span class="linenos">4</span><span class="kr">end</span>
</code></pre></div>
<p>So after all is said and done, we have:</p>
<div class="highlight"><pre><span></span><code><span class="linenos"> 1</span><span class="kd">local</span><span class="w"> </span><span class="nv">vec</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="linenos"> 2</span><span class="w"> </span><span class="nv">__index</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">vec</span><span class="p">,</span>
<span class="linenos"> 3</span><span class="w"> </span><span class="nv">__super</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">obj</span><span class="p">,</span>
<span class="linenos"> 4</span><span class="w"> </span><span class="nv">__call</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">obj</span><span class="p">.</span><span class="py">__call</span><span class="p">,</span>
<span class="linenos"> 5</span>
<span class="linenos"> 6</span><span class="w"> </span><span class="nv">init</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">function</span><span class="p">()</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="kr">end</span><span class="p">,</span>
<span class="linenos"> 7</span><span class="w"> </span><span class="nv">__add</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">function</span><span class="p">()</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="kr">end</span><span class="p">,</span>
<span class="linenos"> 8</span><span class="w"> </span><span class="nv">__sub</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">function</span><span class="p">()</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="kr">end</span><span class="p">,</span>
<span class="linenos"> 9</span><span class="w"> </span><span class="nv">iadd</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">function</span><span class="p">()</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="kr">end</span><span class="p">,</span>
<span class="linenos">10</span>
<span class="linenos">11</span><span class="w"> </span><span class="nv">METATABLE</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">obj</span><span class="p">,</span>
<span class="linenos">12</span><span class="p">}</span>
</code></pre></div>
<p>Now for the magic part. When I call <code>vec()</code>, Lua checks the metatable. (The <code>__call</code> in the main table does nothing!) The metatable is <code>obj</code>, which does have a <code>__call</code>, so Lua calls that function and inserts <code>vec</code> as the first argument. Then <code>obj.__call</code> creates an empty table, assigns <code>self</code> (which is the first argument, so <code>vec</code>) as the empty table’s metatable, and calls the new table’s <code>init</code> method.</p>
<p>Ah, but the new table is empty, so it doesn’t <em>have</em> an <code>init</code> method. No problem: it has a metatable with an <code>__index</code>, so Lua consults that instead. The metatable’s <code>__index</code> is <code>vec</code>, and <code>vec</code> <em>does</em> contain an <code>init</code>, so that’s what gets called. (If there were no <code>vec.init</code>, then Lua would see that vec <em>also</em> has a metatable with an <code>__index</code>, and continued along. That’s why I didn’t need an <code>anise.init</code>.)</p>
<p>That’s also why defining <code>vec:__add</code> works — it puts the <code>__add</code> metamethod into <code>vec</code>, which becomes the metatable for all vector objects, thus automatically making <code>+</code> work on them.</p>
<p>That’s all there is to it. It’s possible to get much more elaborate with this in a number of ways, but this is the bare minimum — and it could still be trimmed down further.</p>
<p>Note that you can’t actually call <code>obj</code> itself. Pop quiz: why not?</p>Eevee gained 3367 experience points2021-01-24T11:35:00-08:002021-01-24T11:35:00-08:00Eeveetag:eev.ee,2021-01-24:/blog/2021/01/24/eevee-gained-3367-experience-points/<p>Eevee grew to level 34!</p>
<p>I super almost forgot to write one of these!</p>
<p>What a very, very long year. I went back through my dev journal to see what I’d done and could not believe most of this happened in the past year. Even stuff from August feels like it must have been at least a year ago.</p>
<p>Eevee grew to level 34!</p>
<p>I super almost forgot to write one of these!</p>
<p>What a very, very long year. I went back through my dev journal to see what I’d done and could not believe most of this happened in the past year. Even stuff from August feels like it must have been at least a year ago.</p>
<hr />
<p>I made our first Steam release: <a href="proxy.php?url=https://store.steampowered.com/app/1259530/Cherry_Kisses/">Cherry Kisses</a>, a polished version of a jam game we made (though didn’t finish in time to get in the jam, oops) the year before. It’s sold pretty decently, especially considering the reduced audience (adult games are hidden on Steam unless you opt into them), so that’s been nice.</p>
<p>I started <span class="caps">HRT</span>! Then I stopped <span class="caps">HRT</span>. Alas.</p>
<p>I dipped my toes into Godot for real this time with <a href="proxy.php?url=https://eevee.itch.io/rogue-ike">Rogue Ike</a>, a Strawberry Jam game that was perhaps much too ambitious for a first (and time-limited) attempt but worked out as a proof of concept. I don’t think I’ll pick this back up until I’ve made something a bit more substantial, though; I’ve got a lot of bits and pieces of Godot code now but still don’t feel like I have a solid grasp of how I’d approach architecture for a new game.</p>
<p>I poured some feelings into a little <span class="caps">PICO</span>-8 game: <a href="proxy.php?url=https://eevee.itch.io/anise-wheres-twig">Star Anise Chronicles: Oh No Wheres Twig??</a>, a charming little platformer about my cat’s fursona. It’s probably my favorite thing I’ve ever released.</p>
<p>I did actually an <a href="proxy.php?url=https://eev.ee/blog/2020/08/04/fox-flux-three-years-later/">incredible lot of work on fox flux</a> over the summer and made massive strides with it in a relatively short span of time? It was a broken hopeless mess at the start of the year, and now it’s… well, not. Better art, better physics, more plot ideas, a lot more little bits and pieces implemented, a whole minigame conceived and mostly implemented, a level tally… it feels like a real game, even!</p>
<p>I also took a crack at a possible port of fox flux to Godot, which was informative both about Godot itself and about designing complex actors in general, but I don’t think I’m going to continue with it. Godot does make some stuff easier, but at the cost of a lot of rough edges that will seriously slow me down — a lot of basic functionality I’ve been taking for granted in my current setup of LÖVE-duct-taped-to-Tiled just does not exist in Godot, and some of the 2D tooling has major oversights that I’d have to work around. Some of this will be fixed in Godot 4, but I don’t want to wait for that just to continue on this game that I’ve already poured a lot of work into. I’ll probably do something simpler in Godot 4 when it comes out.</p>
<p>I poured most of the last four months of the year into a surprise project, by which I mean, I surprised myself by doing it: <a href="proxy.php?url=https://eev.ee/blog/2020/09/26/lexys-labyrinth/">Lexy’s Labyrinth</a>, a web-based and unencumbered Chip’s Challenge 2 emulator — the first of its kind! It still has some teeny compatibility issues, but for the most part it faithfully plays both the original Chip’s Challenge 1 and 2 as well as tons of community levels created over the years. It needs a bit more polish, and then I’m gonna call it “basically finished” and make a bigger effort to drum up interest in it.</p>
<p>I think I worked on baz, the game engine I wanted to make that was intended as a bridge between MegaZeux and PuzzleScript and bitsy? But I haven’t touched it in a while now. I also started a web-based Sudoku player and then lost interest in Sudoku again. And then there was the <span class="caps">AC</span>:<span class="caps">NH</span> companion, which I kept up with until I lost interest in Animal Crossing. Hmmm.</p>
<p>I did dip my toe back into blogging with the well-received <a href="proxy.php?url=https://eev.ee/blog/2020/02/01/old-css-new-css/"><span class="caps">CSS</span> post</a>, and then not so much for a while. I started the “<a href="proxy.php?url=https://eev.ee/blog/2020/11/30/gamedev-from-scratch-0-groundwork/">gamedev from scratch</a>” series to replace the ill-fated book I toyed with writing, but it has yet to see a second installment.</p>
<hr />
<p>I feel like I miss making video games, even though I did rather a lot of it this year. I guess I don’t feel like I <em>released</em> any; Cherry Kisses was an existing game, Rogue Ike didn’t get further than a handful of rooms, fox flux is still quite a ways off from being done, and Lexy’s Labyrinth is really a game <em>engine</em>. That leaves the Star Anise game as the only “““real””” one I released, but that may not be an entirely fair way to gauge how much work I’ve done.</p>
<p>I do miss writing more often. I guess after everything that happened three years ago, I never quite figured out how to reconnect with the universe. Sometimes I go on a tweeting spree for a couple days, and that feels nostalgic, but in general I’ve gotten more withdrawn and don’t quite know how to shake it. I’m still trying.</p>
<p>I like how well Cherry Kisses did, and I’d kinda like to do small adult game releases more regularly — they’re fun to design and write, they make folks happy, and they bring in a steady trickle of sales. I have a concept for one I’m going to start on during <a href="proxy.php?url=https://itch.io/jam/strawberry-jam-5">Strawberry Jam 5</a> next month, but it’s a bit more ambitious, so I might have to do something smaller for Steam purposes this year. Maybe I should take this as an opportunity to get a real foothold in Godot? Cherry Kisses wasn’t terribly complicated; I could recreate something like that without much trouble, and spend some time ironing out wrinkles.</p>
<p>I do want to keep working on fox flux — it’s been just about four years since the jam version now, and I still love the idea and would like to get it seriously going. I’ve spent so much time on engine and design stuff that I still barely have any areas to show!</p>
<p>And of course I would very much love to get that gamedev from scratch series going. I promised one installment per month, and I already missed December because I was neck-deep in Lexy’s Labyrinth, so I really ought to write <em>two</em> in the next week. We’ll see how that goes.</p>
<hr />
<p>I don’t know how I feel about being 34. Solidly in my mid-30s. I still remember the days, twenty years ago now, when I was the youngest person I knew in almost any circles: online, at school, whatever. Now I’m usually one of the oldest, as most folks my age are off with children and careers; whereas I’ve made a life out of making weird stuff on the internet, just like I did as a teenager.</p>
<p>Still, I guess that means I’m exactly where I always wanted to be.</p>
<hr />
<p>Browsers all have autoplay restrictions now, so you’ll have to hit play on this yourself.</p>
<p><audio src="proxy.php?url=/media/2012-01/levelup.ogv" controls autoplay></p>Cherry Kisses, on Steam2020-11-30T16:44:00-08:002020-11-30T16:44:00-08:00Eeveetag:eev.ee,2020-11-30:/blog/2020/11/30/cherry-kisses-on-steam/<div class="prose-full-illustration">
<img alt="Cherry Kisses title screen, showing Cerise at a counter" src="proxy.php?url=https://eev.ee/media/updates/cherry-kisses.png"/>
</div>
<p>🔗 <a href="proxy.php?url=https://store.steampowered.com/app/1259530/"><strong>Steam release</strong></a><br/>
🔗 <a href="proxy.php?url=https://eevee.itch.io/cherry-kisses"><strong>itch release</strong></a></p>
<p>Whoops! I meant to write about this when it originally came out, <em>in April</em>, but never quite got around to collecting my thoughts. Here is a very rushed subset of them.</p>
<p><strong>The game is extremely <span class="caps">NSFW</span></strong>, but the commentary below is not.</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/updates/cherry-kisses.png" alt="Cherry Kisses title screen, showing Cerise at a counter">
</div>
<p>🔗 <a href="proxy.php?url=https://store.steampowered.com/app/1259530/"><strong>Steam release</strong></a><br />
🔗 <a href="proxy.php?url=https://eevee.itch.io/cherry-kisses"><strong>itch release</strong></a></p>
<p>Whoops! I meant to write about this when it originally came out, <em>in April</em>, but never quite got around to collecting my thoughts. Here is a very rushed subset of them.</p>
<p><strong>The game is extremely <span class="caps">NSFW</span></strong>, but the commentary below is not.</p>
<hr />
<h2 id="the-game-itself"><a class="toclink" href="proxy.php?url=#the-game-itself">The game itself</a></h2>
<p>I like the game. It’s essentially a visual novel, but <em>disguised</em>.</p>
<p>I’ve played a decent number of visual novels, and I’ve thought a lot about them and their role as kind-of-games, and I’ve noticed the thorny bits that I don’t like. And my thoughts have circled around the notion of <em>player agency</em>.</p>
<p>Agency is what makes a game feel like a game. You have <em>input</em>, in a broad sense. You can do something to the game, and it will react appropriately (fingers crossed).</p>
<p>This theory explains the awkward position of visual novels. The bulk of the experience is reading a passage, pressing spacebar, and <span class="caps">GOTO</span> 10. You don’t have meaningful input; pressing spacebar isn’t a decision, it’s scrolling.</p>
<p>When you <em>do</em> have input, it generally comes in the form of a menu. But this doesn’t feel like you’re <em>making</em> a choice; it feels like one is being <em>extracted</em> from you in the middle of an otherwise passive reading experience. The base form of the game is reading, and that has been interrupted at a predetermined point to demand something of you. You often don’t have enough information to make a <em>meaningful</em> choice, either, so this becomes a game of saving at each branch and performing an exhaustive depth-first search of the story. As time goes on, you end up skipping through more and more of the early parts, and may hit a point where you go down a decision branch not even remembering what form the story took before you got there.</p>
<p>This is a weird experience.</p>
<p>I wanted to try to improve the feeling of a <span class="caps">VN</span> without altering the substance, so this one is disguised as an <span class="caps">RPG</span>. I mean, not really an <span class="caps">RPG</span>, but that brand of top-down “walk around and interact with stuff” framing.</p>
<p>You play as Cerise, and the entire game takes place in her shop. At any given time, zero or more customers are present, and you can either twiddle your thumbs at the counter or talk to one of them. Whatever you do will generally advance time by an hour, which may change the set of customers; some folks left or arrived while you were busy doing something else. And different folks have different reactions to being ignored, so the whole game becomes one large meta scheduling puzzle.</p>
<p>The thing is, this could’ve been done just as well with a menu at the start of each hour, asking who you want to talk to. The gameplay would’ve been functionally identical. But this scheme feels completely different (at least to me) for several reasons:</p>
<ol>
<li>
<p>Instead of choices being “on top of” the prose, the prose is on top of the choices. It feels like the choices you make <em>cause</em> the prose to happen, rather than being forks in the middle of a river you can’t escape. You can wander around the shop as long as you like, taking breathers, and time will not pass until <em>you</em>, the human at the controls, cause something to happen. (You could say the same about a menu in a <span class="caps">VN</span>, but there you can’t do anything <em>else</em> either; the entire game is frozen until you interact with this modal dialog.)</p>
</li>
<li>
<p>You can do other things. Not <em>many</em>, granted, but you can examine every single object in the shop, and they all have different descriptions (even if they look identical). A typical visual novel doesn’t give you the opportunity to go on frivolous tangents, but I think a big part of games is being able to forget about the progression for a minute and fuck around with something that looks interesting. Stop and smell the roses, in this case literally.</p>
</li>
<li>
<p>A menu spells out all possible options with equal priority. They’re just items in a list, after all. A physical world, on the other hand, can add subtle differences — choices may be more or less obvious, more or less compelling, or be presented in some way that adds to the narrative. For example, while customers tend to show up at arbitrary spots throughout the shop, your girlfriend Lexy will wait for you right behind the counter, suggesting a more personal relationship even if you don’t yet know who she is. Or consider the ubiquitous option of ignoring <em>everyone</em> in the shop and passing time at the counter instead. That would usually be pointless, so it would be obnoxious to list in every single menu, but having it as an option in-world makes it less obvious… which is perfect puzzle fodder. Just saying.</p>
</li>
</ol>
<p>As an added bonus, every character in the game has a “happiness” rating from -3 to 3. If you can help them with their problems, their happiness will increase. The numbers are largely arbitrary, but you do get a final score tally at the end, and that gives some sense of measured accomplishment that’s more nuanced than a mere good/bad ending. You can ignore it altogether and be happy with the story you got, <em>or</em> you can go down the rabbit hole and try to find the unique path through the game that will make everyone happy and get you a perfect score.</p>
<p>These feel like really subtle design decisions that have an equally subtle impact on the experience. I don’t know what impact it had on anyone else, but <em>I</em> really liked the results. I didn’t mind playing through the game a gazillion times while I was developing it, because it’s just nice to play. The story isn’t especially deep, but it has a lot of little lighthearted interactions with a variety of characters, and sometimes different threads impact each other in really subtle ways. Sometimes I ran across an interaction I’d forgotten I’d written! It feels like the kind of story game that you <em>can’t</em> merely grind every ending out of, one that always has a chance to surprise you a little.</p>
<p>I still have other ideas for making narrative games that feel more player-controlled, so fingers crossed that I can pull them off.</p>
<h2 id="steam-numbers-business"><a class="toclink" href="proxy.php?url=#steam-numbers-business">Steam, numbers, business</a></h2>
<p>This is the first game I’ve put on Steam, a platform I’ve long had mixed feelings about. On the one hand, it’s cool that video games have something like a package repository. On the other hand, that package repository is owned and controlled by a single company that sits back and rakes in billions (30% off of every sale!) from a glorified <span class="caps">FTP</span> server, something that Linux distributions do for free. And it’s normalized casual <span class="caps">DRM</span>, which I do not enjoy. (If I did it right, then manually running Cherry Kisses while Steam is closed should simply <em>run the game</em> without interacting with Steam at all.)</p>
<p>On the other hand, I can’t deny the impact. The Steam release earned more in its first two weeks than the itch release did in more than a year.</p>
<p>…okay, that isn’t an entirely fair comparison. The itch release also had a free “demo” version that was exactly like the “real” version, only with lower-resolution artwork. <em>Loads</em> of people played that (almost 20k downloads as of now), and in retrospect we may have shot ourselves in the foot a bit by offering a free version. But I do <em>like</em> when people can play my games, and releasing anything only in a paid form feels like extorting people out of their money.</p>
<p>I am not good at business. It mostly feels bad.</p>
<p>Despite that, the game has somehow grossed a bit over $10k in the last eight months (which shrinks to $6k net after the Steam tax, <span class="caps">VAT</span>, and refunds). That’s not a <em>windfall</em>, but it’s far more than I ever expected to earn on the back of a month-long jam game, and it all went to paying our 2019 taxes so it’s like nothing ever happened. It certainly makes me optimistic about selling something meatier.</p>
<h2 id="creating-the-steam-release"><a class="toclink" href="proxy.php?url=#creating-the-steam-release">Creating the Steam release</a></h2>
<p>We did update the game somewhat for Steam, a process that ended up consuming almost <em>a month</em> somehow and still didn’t cover everything we wanted. The most obvious in-game things were the addition of character profiles, an image gallery, and an options menu — which is to say, all <span class="caps">UI</span> things, which I had to build in LÖVE, by hand, which was an incredible pain in the ass. But it works, somehow.</p>
<p>Of course I also added a bunch of Steam achievements, which were kinda fun to decide upon. It’s a story game, so they’re mostly of the form “encounter this bit of the story”, but that’s fine?</p>
<p>But oh boy, the thing that really took the longest time was linking to Steam at all. You get a <span class="caps">DLL</span>/<span class="caps">SO</span>, some header files, and some hit-or-miss <a href="proxy.php?url=https://partner.steamgames.com/doc/sdk">documentation</a>, and the rest is up to you.</p>
<p>The library is, of course, designed for C++.</p>
<p>I am not using C++. I am using <em>Lua</em>.</p>
<p>This posed something of a problem.</p>
<p>I prefer not to touch C++ with a ten-foot pole, so writing some glue on the C++ side did not sound appetizing. (That would’ve also left me with the difficult problem of <em>compiling</em> that code for platforms I do not own or develop on.) That left me with binding to the Steam <span class="caps">API</span> from the Lua side.</p>
<p>After several days of Googling, finding years-old projects that promised to do this, and completely failing to get anywhere at all with them, I resigned myself to writing something from scratch. LÖVE uses LuaJIT, which comes with the excellent <span class="caps">FFI</span> library, meaning I could bind to C with nothing more than a header file.</p>
<p>The Steam <span class="caps">API</span> <em>does</em> have a C compatibility layer, but it is basically not documented, so I had to do some guesswork to get from the documentation to the parts I actually needed. Also, the core of the Steam <span class="caps">API</span> is this hokey async messaging system built out of macros and C++ metaprogramming, so I had to do a clumsier polling thing using disparate parts of the C <span class="caps">API</span> instead. I finally discovered that there’s example code in a big honking comment in the headers themselves, except <strong><em>the example code is wrong</em></strong>, so I had to fix that as well. Plus all the obtuse bugs like with padding on different platforms which for some reason is baked into the messages that Steam sends because C programmers don’t know how to actually fucking serialize anything. It was an adventure!!</p>
<p>But after all that, I managed to get achievements working, and also leaderboards. Neat, cool, etc.</p>
<p>The game <em>does</em> leak coroutines indefinitely if it’s run through Steam but can’t connect, though. Sorry.</p>
<p>Man. The Steam website has so many features, and the documentation explains them all in one succinct list, but fuck me if I can actually <em>find</em> any of them. So many things are not linked from obvious places; there have been many times I knew a particular page existed but <em>could not</em> figure out how to get there, and ultimately I started relying on address bar history instead of trying to navigate this website.</p>
<p>And so many features are awkwardly built on top of older features that are actually something completely different. Like we have a <a href="proxy.php?url=https://store.steampowered.com/developer/floraverse">“developer” page on Steam</a>, but the only part of it we can really control is a single line of plain text at the top. If you go to the “about” tab, it just shows you that line again! That’s all we can put there! You have to click “visit group page” (why would you do that??) in the sidebar of <em>that</em> page to actually get to something we can control.</p>
<p>In stark contrast to itch, Steam <em>really</em> wants your store page to look like a Steam store page and not like a your-game store page. Your artwork (and there is so much artwork) has to be manually approved by a human, and along the way I discovered some extremely unintuitive rules, like that the library header has to be <span class="caps">SFW</span> even though it’s only visible to people who already own the game. Store pages also have a “legal” section, but I couldn’t list open source libraries I used (and their licenses) in that section, because I’m not allowed to have links. Like, at all. They <em>really</em> don’t want you to have links. Games exist independently of the humans that made them in the world of Steam; they are isolated jewels floating in a vast space that is linked directly to gaben’s bank account.</p>
<p>I cannot comprehend how weirdly low-key hostile the whole experience felt. All so they could take a third of my money.</p>
<p>Oh, and there’s no Mac release, because I do not have a Mac on which to sign Mac software and do not wish to pay Apple for the privilege, and Mac software does not run any more if it’s not signed. Sorry. Yes, I fucking know about fucking right-click open, please stop fucking telling me about that, that is not useful for software that is run from <em>someone else’s launcher</em>.</p>
<h2 id="reception"><a class="toclink" href="proxy.php?url=#reception">Reception</a></h2>
<p>People seem to like it?? I mean, I’ve had a dozen or so people tell me to my face that they had an <em>especially</em> good experience with it, that it was cozy and upbeat and just <em>nice</em>. For a few of them, it apparently helped ease some aversion they’d had to sex, simply by showing it playing out well.</p>
<p>It’s funny that I thought so hard about the general design and how agency worked and all that, but 99% of the feedback has been about the feeling of the prose itself — something that just kinda fell out of my fingers. I guess I’m not <em>surprised</em> — after all, if these players thought as hard about game design as I do, they’d probably be designing games.</p>
<p>As of this writing, there have been 19.5k downloads on itch and 1750 sales on Steam. Of the Steam sales, a hair under 80% of the people who own the game have actually played it, so if I extrapolate wildly, maybe 17,000 people have played it.</p>
<p>But I don’t see anyone <em>talk</em> about it outside of my immediate circles, which feels a bit weird. Maybe? I’m not sure what the “normal” amount of conversation about an admittedly niche game is. I don’t know how things really spread by word of mouth, and I thought this might be an opportunity to gleam some insight about that, but it has not visibly materialized even though the game is being bought by people I don’t personally know.</p>
<p>On the one hand, it’s a sex game, so many folks are less likely to talk about it. (A couple people even specifically asked if Steam has a way to hide what game you’re playing from your friends — and, alas, it does not.) On the other hand, it’s a <em>furry</em> sex game, and furries are traditionally not so tight-lipped.</p>
<p>Maybe there’s not that much to say; the impact it’s had on people I know has been fairly personal, and if it didn’t have that kind of impact then it’s just a cute little story game.</p>
<h2 id="lessons-learned"><a class="toclink" href="proxy.php?url=#lessons-learned">Lessons learned</a></h2>
<p>I have no idea. There are so many confounding factors here that I don’t know how to conclude anything.</p>
<p>I guess I’m pleasantly surprised by how many people bought a fairly short game for $7. As it turns out, people will give you money for a thing if you ask for it? That’s nice to know.</p>
<p>Releasing on Steam is such a huge pain in the ass lol.</p>
<p>Sales spiked right at the beginning and then flattened fairly quickly, but it still sells a few copies a week, so it looks like it’ll be a little trickle of income for a while. It’d be cool to get a few medium-sized games on Steam as an extra source of income. I suspect porn games have a bit more staying power, too.</p>
<p>Writing <span class="caps">UI</span> by hand sucks ass. I gotta switch to Godot asap.</p>Gamedev from scratch 0: Groundwork2020-11-30T14:58:00-08:002020-11-30T14:58:00-08:00Eeveetag:eev.ee,2020-11-30:/blog/2020/11/30/gamedev-from-scratch-0-groundwork/<p>You may recall that I once had the ambitious idea to write a book on game development, walking the reader through making simple games <em>from scratch</em> in a variety of different environments, starting from simple level editors and culminating in some “real” engine.</p>
<p>That never quite materialized. As it turns out, writing a book is a huge slog, publishers want almost all of the proceeds, and LaTeX is an endless rabbit hole of distractions that probably consumed more time than actually writing. Also, a book about programming with no copy/paste or animations or hyperlinks kind of sucks.</p>
<p>I thus present to you Plan B: a series of blog posts. This is a narrative reconstruction of a small game I made recently, <a href="proxy.php?url=https://eev.ee/blog/2020/05/10/star-anise-chronicles-oh-no-wheres-twig/">Star Anise Chronicles: Oh No Wheres Twig??</a>. It took me less than two weeks and I kept quite a few snapshots of the game’s progress, so you’ll get to see a somewhat realistic jaunt through the process of creating a small game from very nearly nothing.</p>
<p>And unlike your typical programming tutorial, I can <em>guarantee</em> that this won’t get you as far as a half-assed Mario clone and then abruptly end. The game has original art and sound, a title screen, an ending, cutscenes, dialogue, <span class="caps">UI</span>, and more — so this series will necessarily cover how all of that came about. I will tell you why I made particular decisions, mention planned features I cut, show you the tradeoffs I made, and confess when I made life harder for myself. You know, all the stuff you <em>actually go through</em> when doing game development (or, frankly, any kind of software development).</p>
<p>The target audience is (ideally) anyone who knows what a computer is, so hopefully you can follow along no matter what your experience level. Enjoy!</p>
<hr/>
<p>This is <strong>part zero</strong>, and it’s mostly introductory stuff. Please don’t skip it! I promise there’s some meat in the latter half.</p>
<p>You may recall that I once had the ambitious idea to write a book on game development, walking the reader through making simple games <em>from scratch</em> in a variety of different environments, starting from simple level editors and culminating in some “real” engine.</p>
<p>That never quite materialized. As it turns out, writing a book is a huge slog, publishers want almost all of the proceeds, and LaTeX is an endless rabbit hole of distractions that probably consumed more time than actually writing. Also, a book about programming with no copy/paste or animations or hyperlinks kind of sucks.</p>
<p>I thus present to you Plan B: a series of blog posts. This is a narrative reconstruction of a small game I made recently, <a href="proxy.php?url=https://eev.ee/blog/2020/05/10/star-anise-chronicles-oh-no-wheres-twig/">Star Anise Chronicles: Oh No Wheres Twig??</a>. It took me less than two weeks and I kept quite a few snapshots of the game’s progress, so you’ll get to see a somewhat realistic jaunt through the process of creating a small game from very nearly nothing.</p>
<p>And unlike your typical programming tutorial, I can <em>guarantee</em> that this won’t get you as far as a half-assed Mario clone and then abruptly end. The game has original art and sound, a title screen, an ending, cutscenes, dialogue, <span class="caps">UI</span>, and more — so this series will necessarily cover how all of that came about. I will tell you why I made particular decisions, mention planned features I cut, show you the tradeoffs I made, and confess when I made life harder for myself. You know, all the stuff you <em>actually go through</em> when doing game development (or, frankly, any kind of software development).</p>
<p>The target audience is (ideally) anyone who knows what a computer is, so hopefully you can follow along no matter what your experience level. Enjoy!</p>
<hr />
<p>This is <strong>part zero</strong>, and it’s mostly introductory stuff. Please don’t skip it! I promise there’s some meat in the latter half.</p>
<h2 id="table-of-contents"><a class="toclink" href="proxy.php?url=#table-of-contents">Table of contents</a></h2>
<p>Here’s what you have to look forward to (though it is of course a <span class="caps">WIP</span> until the series is done). Occasionally there’ll be a <em>snapshot</em> of the game, but these were made on a whim during development and aren’t particularly meaningful as milestones.</p>
<p>For reference, I started working on the game the morning of April 29, and I released it the night of May 10, for a total of twelve days.</p>
<ul>
<li><strong>Part 0: Groundwork</strong> (you are here) — introduction, tour of <span class="caps">PICO</span>-8, putting something on the screen, moving around, measuring time, simple sprite animation</li>
<li><a href="proxy.php?url=https://eev.ee/blog/2021/01/26/gamedev-from-scratch-1-scaffolding/">Part 1: Scaffolding</a> — structure, objects in Lua, a taste of collision detection</li>
</ul>
<!--
* Part 1: objects, basic architecture, collision [snapshot v1 — Apr 29, 7:31pm]
* custom palette
* collision detection
* giving anise abilities
* a HUD for the abilities
* doors
* glasses
* NPCs
* a title screen and scene switching
* a background
* telepawt
* landing particles
* inventory switch animation
* sprite anchors
* smoke vent
* text shadows
* screen transitions and a parallax background
* scrolling dialogue
* stars and particle effects
* remaining NPCs
* intro cutscene
* items and animations for using them
* sparkles
* most of the first half of the plot
* converting to coroutines
* first pass at code size
* flags for map connections
* locked "doors"
* credits
* waterfall
*
-->
<!--
Apr 29 19:31 anise1.5-v1.p8 sort, vec, mobactor (including collision), anise moves around
Apr 30 22:53 anise1.5-v2.p8 fix partial movement (?), anise abilities, doors, ui for them, pickup i think
May 3 22:55 anise1.5-v3.p8 glasses, buttons, npcs, scenes, basic title screen, background, telepawt, shutter util type, stars?
May 4 02:04 anise1.5-v4.p8 inventory animation, anchors back, smoke vent
May 5 08:12 anise1.5-v5.p8 text shadow, screen transitions and parallax background, scrolling dialogue, stars, remaining lunekos, intro cutscene
May 7 00:49 anise1.5-v6.p8 items and animations for them, sparkles, i think the entire first half,
May 7 10:44 anise1.5-v7.p8 code size, coros instead of shutter, map connections as flags, keys and locks, waterfall,
May 7 14:57 anise1.5-v8.p8 more code size mostly
May 8 05:37 anise1.5-v8a.p8 coro dialogue, ?
May 9 04:55 anise1.5-v9.p8
May 10 11:53 anise1.5-v10.p8
May 10 12:37 anise1.5-v11.p8
-->
<h2 id="introduction"><a class="toclink" href="proxy.php?url=#introduction">Introduction</a></h2>
<p><strong>This is not a tutorial.</strong> Please set your expectations accordingly. Honestly, I don’t even like tutorials — too many of them are framed as something that will teach you a skill, but then only tell you what buttons to press to recreate what the author already made, with no insight as to why they made their decisions or even why they pressed those particular buttons. They often leave you hanging, with no clear next steps, no explanation of what to adjust to get different results.</p>
<p>I’ve never seen a platformer tutorial that actually produced a finished game. Most of them give you just enough to have a stock sprite (poorly) jump around on the screen, perhaps collect some coins, and that’s it. How do you fix the controls, add cutscenes, even make a damn title screen? That’s all left up to you.</p>
<p>This is something much better than a tutorial: a <em>story</em>. I made a video game — a real, complete video game — and I will tell you everything I can remember doing <em>and thinking</em> along the way. Every careful decision, every rushed tradeoff, every boneheaded mistake, every weird diversion. I don’t guarantee that anything I did is necessarily a <em>good</em> idea, but everything I did is <em>an</em> idea, and sometimes that’s all you need to get the gears turning.</p>
<p>If you’re interested in making a video games, I don’t promise that this series will <em>teach</em> you anything. But with a little effort, you can probably <em>learn</em> something. And to be frank, if you’re starting with zero knowledge but still manage to muddle through the whole series, you’ve got more than enough curiosity and determination to succeed at whatever you feel like doing.</p>
<p>The game in question is <a href="proxy.php?url=https://eev.ee/blog/2020/05/10/star-anise-chronicles-oh-no-wheres-twig/">Star Anise Chronicles: Oh No Wheres Twig??</a>, which I made with the <a href="proxy.php?url=https://www.lexaloffle.com/pico-8.php"><span class="caps">PICO</span>-8</a>. (If you are from the future, I specifically used version 0.2.0i; later versions may have added conveniences I’m not using.) This is not a whizbang fully-featured game engine like <a href="proxy.php?url=https://godotengine.org/">Godot</a> or <a href="proxy.php?url=https://unity.com/">Unity</a>. If I want to draw something, I have to draw it myself. If I want physics, I have to write them myself. If I want shaders… well, that’s not going to happen, but a little ingenuity can still go a long way.</p>
<p>And that kind of ingenuity is what makes game development appealing to me in the first place. It’s one big puzzle: given the tools I have, what’s the most interesting thing I can make with the least amount of hapless flailing? That question will come up a number of times in this series.</p>
<p>If any of this sounds appealing to you, keep reading! Follow along if you can. You can get the <span class="caps">PICO</span>-8 (tragically not open source) for $15, and chances are you already own it — it was in the itch.io <a href="proxy.php?url=https://itch.io/b/520/bundle-for-racial-justice-and-equality"><span class="caps">BLM</span> bundle</a>, so if you bought that, you’re free to download it whenever you want.</p>
<h3 id="conventions"><a class="toclink" href="proxy.php?url=#conventions">Conventions</a></h3>
<p>In order to replicate the experience of reading the book, I’m porting these little “admonition” boxes from what I’d started. I have a somewhat meandering writing style, and hopefully these will help get tangents out of the main text, while also better highlighting warnings and gotchas.</p>
<p>Here they are, in no particular order:</p>
<aside class="aside--well-actually">
<p>This indicates that I have just told you a little white lie for the sake of simplicity, like a math teacher fibbing that you can't take the square root of negative numbers, but my conscience will not let it go uncommented upon.</p>
<p>(Actually, Twitter stopped using eggs as default avatars in April 2017.)</p>
</aside>
<aside class="aside--note-from-future">
<p>While I did just accurately describe my thought process <em>at the time</em>, I have arrived from the future to let you know I actually made a very poor decision. Tragically, my past self cannot see this warning, and we must now together watch her stumble into her impending doom.</p>
</aside>
<aside class="aside--computers-are-bad">
<p>Computers are wonderful tools, but sometimes they are dead set on sabotaging you and everything you hold dear for no good reason. Beware! There's some asinine problem here that has no good reason to exist.</p>
</aside>
<aside class="aside--fascinating-tangent">
<p>This has nothing to do with whatever I was just talking about and you can almost certainly ignore it without missing anything important, but I couldn't resist telling you about it.</p>
</aside>
<aside class="aside--matter-of-taste">
<p>Something I just stated as though it were fact is really a matter of opinion (and, most likely, heated debate), but this is my blog, so I can say my opinion is right and delete all the comments that try to argue with me.</p>
</aside>
<aside class="aside--look-out">
<p>Programming is full of pitfalls, and game development is practically overflowing with them. Like that one you're falling into right now.</p>
</aside>
<aside class="aside--tricky-tradeoff">
<p>You can't always get what you want. But if you try sometimes, you might find, you can get half of each thing you wanted and it'll be good enough.</p>
</aside>
<aside class="aside--deceptively-difficult">
<p>You'd think this should take like two minutes, right? Great news: it's gonna consume the next ten hours. Some things are much harder than they look; don't lose heart if you struggle!</p>
</aside>
<p>I reserve the right to invent more, if they’re needed and/or funny.</p>
<!--
warning: this is hard or distracting or whatever
don't worry about it: this is a hard part that isn't really important, or i made it more complicated than necessary
- other suggestions for myself: gotcha, useful context, entertaining mistakes, performance, tip/hint/handy trick/nice shortcut (power user?), "good enough for now", doing it right™, quick and dirty... easy pitfall,
- go look it up yourself
-->
<h3 id="setting-expectations-again"><a class="toclink" href="proxy.php?url=#setting-expectations-again">Setting expectations, again</a></h3>
<p>Game development is about a lot more than programming, but this <em>will</em> contain an awful lot of programming. The <span class="caps">PICO</span>-8 in particular tends to blur the lines between code and assets if you want to do anything fancy.</p>
<p>That puts me in a tricky position as an author. I want this to be accessible to people with little or no programming experience, but I can’t realistically explain every single line of code I write, or this series will never end (and will be more noise than signal for intermediate programmers).</p>
<p>Thus, I’m <strong>trusting you</strong> to look up basic concepts on your own if you need to. I’m writing this to fill a perceived gap, so I’ll try to focus on the gaps — finding resources on from-scratch collision detection is a crapshoot, but the web is awash in explanations of what a “variable” is. <span class="caps">PICO</span>-8 uses a programming language called <a href="proxy.php?url=https://www.lua.org/">Lua</a> which is pretty simple and easy to pick up, so if you’re having trouble, maybe thumb through the <a href="proxy.php?url=https://www.lua.org/pil/contents.html">Programming in Lua</a> book a bit too.</p>
<p>Of course, if you’re just here for the ride and not too worried about writing your own game, you can skip ahead whenever you like. I’m not your mom.</p>
<p>(Oh, and if you’ve used Lua before, you should know that <span class="caps">PICO</span>-8’s Lua has been modified from stock Lua. The precise list of changes would be a big block of stuff in the middle of this already too long intro, so I’ve put it <a href="proxy.php?url=#appendix-pico-8-lua-extensions">at the bottom</a>. The upshot is: numbers are fixed-point instead of floating-point, you can use compound assignment, and the standard library is almost completely different.)</p>
<p>That’s probably enough words with no pictures. Time to get started.</p>
<h2 id="the-pico-8"><a class="toclink" href="proxy.php?url=#the-pico-8">The PICO-8</a></h2>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/gamedev-from-scratch/pico8-startup.png" alt="A fresh PICO-8 window, with white old-school text on a small black screen and a command prompt">
</div>
<p>As mentioned, this is a game built with the <span class="caps">PICO</span>-8. I promised I’d tell you a story, but I can’t even explain why I chose <span class="caps">PICO</span>-8 if you don’t know what the thing <em>is</em>.</p>
<p><span class="caps">PICO</span>-8 is a “fantasy console” — a genre that it pioneered. It has a fixed screen size, its own palette, its own font, a little chiptune synthesizer, its own idea of what buttons the player can press, and so on. It’s like an emulator for an 8-bit handheld that doesn’t actually exist, plus a bunch of relatively friendly tools for making cartridges for that handheld. It even has some arbitrary limitations to preserve that aesthetic. (I carefully avoid calling them <em>artificial</em> limitations, because there <em>are</em> some technical reasons for them, and a lot of programmers do a thing with their face if you say “artificial” to them. Like you’ve just spat in their lunch.)</p>
<p>If you’ve got <span class="caps">PICO</span>-8 open, you can type <code>splore</code> at this little command prompt to open the cartridge explorer, which lets you download and play cartridges that have been posted to the <span class="caps">PICO</span>-8 <span class="caps">BBS</span> (forum). You might want to try a few to get a sense of what the <span class="caps">PICO</span>-8 can do, though bear in mind that some of the best games are incredible feats of ingenuity and not representative. A good place to start is the “featured” tab, which lists games that… I believe have been hand-picked as high-quality? Some suggestions:</p>
<ul>
<li>
<p>Star Anise Chronicles: Oh No Wheres Twig is in there, as is our older (and first!) game <a href="proxy.php?url=https://eevee.itch.io/under-construction">Under Construction</a>.</p>
</li>
<li>
<p>The original <span class="caps">PICO</span>-8 version of Celeste, if you weren’t aware of its origins.</p>
</li>
<li>
<p>Dusk Child, one of the earliest games I played and a big inspiration — it’s pretty and expansive, but doesn’t do anything I couldn’t figure out.</p>
</li>
<li>
<p>Just One Boss, which is just so damn <em>crisp</em>.</p>
</li>
<li>
<p>Dank Tomb, a dungeon crawler with absolutely beautiful lighting effects.</p>
</li>
<li>
<p>PicoHot, which is absolute fucking nonsense how dare you.</p>
</li>
</ul>
<p>Note that when playing most games, the <span class="caps">PICO</span>-8 functions as though it only had six buttons: a directional pad bound to the arrow keys, and “O” and “X” buttons bound to the <kbd>Z</kbd> and <kbd>X</kbd> keys. Most games refer to those buttons by name (the <span class="caps">PICO</span>-8 font has built-in symbols for them) rather than keyboard key, since you might be playing on a controller or with some other bindings. You can always press <kbd>Esc</kbd> for the built-in menu.</p>
<p>…</p>
<p>Had fun? Great! Pressing <kbd>Esc</kbd> takes you back to the prompt. From there, you can press <kbd>Esc</kbd> again to switch to the editor (and vice versa).</p>
<p>Now, this is not a <span class="caps">PICO</span>-8 tutorial. But the <span class="caps">PICO</span>-8’s design and constraints <em>immensely</em> impact how much I could do and how I planned to do it, so I can’t very well explain my thought process without that context. Luckily, all the code and assets for the last game you played stay loaded, so I might as well give you the whirlwind tour. Even if you’re not following along with an actual copy of <span class="caps">PICO</span>-8, you should keep reading so you understand what I’ve got to work with.</p>
<h3 id="code-editor"><a class="toclink" href="proxy.php?url=#code-editor">Code editor</a></h3>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/gamedev-from-scratch/pico8-editor-code.png" alt="A very small text editor, populated with code">
</div>
<p>This is the code editor, a very tiny text editor. If you’ve loaded Under Construction, feel free to page through and see what I did. (Keyboard shortcuts help a lot; see <a href="proxy.php?url=https://www.lexaloffle.com/pico-8.php?page=manual">the manual</a> for a full list of them. There are also some <a href="proxy.php?url=https://wh0am1.dev/pico8-api/">cheat sheets</a> floating around, though they focus more on programming capabilities.)</p>
<p>You may have noticed the ominous <code>7695/8192</code> in the bottom right. That’s hinting at one of the <span class="caps">PICO</span>-8’s limitations: the <em>token count</em>. A cartridge’s source code cannot exceed 8192 tokens, or it will not run at all. A “token” is, in general terms, a single “word” of code — a number like <code>133</code>, a name like <code>animframedelay</code>, an operator like <code>+</code>, a keyword like <code>function</code>, and so on. The term “token” is borrowed from the field of parsing, which is an entire tangent you are free to look up yourself.</p>
<p>The <span class="caps">PICO</span>-8’s definition of “token” is slightly different from its typical usage and includes a few exceptions. The common Lua keywords <code>local</code> and <code>end</code> don’t count at all; nor do commas, periods, semicolons, or comments. A string of any length is one token. A <em>pair</em> of parentheses, brackets, or braces only counts as one token. Negative literal numbers (e.g., <code>-25</code>) are one token.</p>
<aside class="aside--fascinating-tangent">
<p>I suspect the reason for this is to emulate, very roughly, what it was like to write code for 8-bit hardware. If you've read any of my posts on my brief (and aborted) dive into Game Boy development, you've seen it was all written in assembly, where everything is built out of very simple terse instruction like <code>add a, 8</code>. More math means more instructions, and each instruction takes up a byte or two. In PICO-8, more math means more <em>tokens</em>, so it works out similarly. Jumping to a label in assembly only needs space for the jump, not the label, so the Lua equivalent of <code>end</code> doesn't take a token. Nesting is meaningless in assembly, so parentheses only count as one (eh). Reading from fields is an address addition plus a read, so the <code>.</code> in the middle shouldn't count. And so on.</p>
<p>Ironically, there is <em>vastly</em> more space on a Game Boy than in the PICO-8 — the original Pokémon games had a whopping half a meg to work with, eight times more than the PICO-8's 64KiB.</p>
</aside>
<p>The token limit is the most oppressive of the limits on your code, but there are two others. The full size of your code cannot exceed 64KiB, though in practice I’ve never come anywhere near that size and I think you’d only approach it if you were committing some serious shenanigans. More of concern, the <em>compressed</em> size of your code cannot exceed 15,616 bytes. I do wind up battling that one near the end of this project (as I did with Under Construction), and it can be extra frustrating since it’s hard to gauge exactly what impact any particular change will have on compression. Thankfully, and unlike with the token limit, the <span class="caps">PICO</span>-8 will still run a game that’s over the compressed size; it just physically cannot export it to a cartridge.</p>
<p>Incidentally, you can use <kbd>Alt</kbd> and an arrow key to move between the editors.</p>
<h3 id="sprite-editor"><a class="toclink" href="proxy.php?url=#sprite-editor">Sprite editor</a></h3>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/gamedev-from-scratch/pico8-editor-sprite.png" alt="A very small sprite editor, showing the mole player character from Under Construction">
</div>
<p>Here we have a tiny pixel art editor. As you might have guessed, the “native” size for a tile is 8 × 8 pixels, though you can use the bottom of the two sliders to edit bigger blocks of tiles at a time. (The screen is 128 × 128 pixels, or 16 × 16 tiles.) You have at your disposal a spritesheet of 256 such tiles, which are arranged at the bottom of the screen in four tabs of 64 tiles each. <code>001</code> here is the tile number. Each tile has its own set of 8 flags you can toggle on and off, which are represented by the eight circles just above the tabs; here, all the flags are off. The flags do nothing by themselves, but you can use them for whatever you like, and they turn out to be pretty handy.</p>
<p>The palette is 16 colors, as shown. There are 16 more colors on the “secret palette” which I’ll be dipping into later, but you can only swap them in; you can never have more than 16 distinct colors on screen at the same time. This is reminiscent of how some early systems actually worked.</p>
<h3 id="map-editor"><a class="toclink" href="proxy.php?url=#map-editor">Map editor</a></h3>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/gamedev-from-scratch/pico8-editor-map.png" alt="A very small map editor, showing the upper left of a cave-like area from Under Construction">
</div>
<p>The map editor edits the map. You only get one; if you want to carve it up somehow, that’s up to you. It’s extremely simple: you have a grid of 128 × 64 tiles (that’s 8 × 4 screenfuls), and you can pick which tile goes in each cell. No layers, no stacking, no two things in the same cell. You can pan around with the middle mouse button and zoom with the mouse wheel (or check the manual for the keyboard equivalents).</p>
<p>The especially nice thing about the map is that you can draw entire blocks of it with the built-in <code>map</code> function, which saves a whole lot of tokens over drawing a bunch of tiles by hand. Even if you’re making a game that doesn’t have a literal map, it’s a convenient way to define and draw blocks of multiple tiles.</p>
<aside class="aside--fascinating-tangent">
<p>Actual hardware like the Game Boy required you to put "most" of your tiles in a grid, with a limited number drawn on top wherever you want. The PICO-8 doesn't have this restriction — you can draw whatever you want at any position — but the convenience of drawing tiles en masse from the map still acts as gentle guidance towards that aesthetic.</p>
</aside>
<p>The catch is that the bottom half of the spritesheet and the bottom half of the map are <em>shared</em>, so you can’t actually have a full map and a full set of tiles in the same cartridge. You could have a full 8 × 4 map and 128 tiles, or you could have a full set of 256 tiles but only an 8 × 2 map, or you can split the space up somehow, but you can’t have the maximum of both. Drawing in the bottom half of one will immediately update the other with garbage. It’s beautiful, actually, if you’re into the aesthetic of arbitrary memory being drawn as tiles.</p>
<p>If you have a cartridge open, you can see this yourself: check out the bottom half of the map (it helps to use <kbd>Tab</kbd> or the buttons in the upper left to hide the tile palette) and tabs 2 and 3 of the sprite editor. If they’re not both completely empty, <em>something</em> will be full of garbage. Try drawing in one or the other, if you like, and you’ll see the other update with junk. That’s the memory layout of pixel data being interpreted as map data, or vice versa. Cool, right?</p>
<aside class="aside--fascinating-tangent">
<p>If you hadn't guessed by now, the PICO-8 has its own faux ROM/RAM layout, which you can even inspect and modify directly, so all these numbers aren't chosen just for kicks. You can't have more than 256 tiles because the map uses one byte per tile; you can't have both a full map and full spritesheet because the shared section literally occupies the same area of ROM, and there's no room for expansion because the 16-bit address space is full! This is the same reason the compressed code limit is the oddball size of 15,616 bytes — that's how much space was left over once everything else was accounted for.</p>
<p>Boy, I sure am using these things a lot already.</p>
</aside>
<h3 id="sound-editor"><a class="toclink" href="proxy.php?url=#sound-editor">Sound editor</a></h3>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/gamedev-from-scratch/pico8-editor-sound-pitch.png" alt="A very small sound editor, showing a sound as bars representing pitch">
<img src="proxy.php?url=https://eev.ee/media/gamedev-from-scratch/pico8-editor-sound-tracker.png" alt="The same sound, but shown using a tracker-like interface">
</div>
<p>The sound editor (or <span class="caps">SFX</span> editor) does a lot, despite being very simple conceptually, and it can be a little intimidating if you’ve never worked with sound or music before. These screenshots are the two display modes, “pitch mode” and “tracker mode” — allegedly pitch mode is more suitable for sound effects and tracker mode is more suitable for music, but I honestly have no idea how anyone does anything in pitch mode, and I use tracker mode for both. Your mileage may vary. As with the map editor, use <kbd>Tab</kbd> or the buttons in the top-left to switch views.</p>
<p>There are 64 sound effects to work with, each consisting of 32 notes played by a little chiptune synth. Notes consist of a pitch (i.e., the actual note being played), an instrument, the volume, and an optional effect.</p>
<p>I could say an awful lot about sound and chiptunes and what any of this means, but this is not a chiptuning tutorial, so I’ll save that for when I actually made some sounds for the game. Do feel free to mess around here, though.</p>
<p>There’s also a music editor, but all it does is arrange several sound effects to play at the same time, so it’s not especially interesting.</p>
<p>And that’s everything at my disposal! I guess that means it’s time to get started, for real. Go back to the command prompt and use <code>reboot</code> to get a fresh blank cartridge, if you’re planning on following along.</p>
<h2 id="inspiration"><a class="toclink" href="proxy.php?url=#inspiration">Inspiration</a></h2>
<p>The first step to making a game is having a game you want to make.</p>
<p>I started on this at the end of April, after a very rushed month spent preparing the <a href="proxy.php?url=https://eev.ee/blog/2020/11/30/cherry-kisses-on-steam/">Steam release of Cherry Kisses</a>. I was pretty pumped about having just published something in a very visible place for the first time, and I wanted to keep that energy going, but I didn’t want to immediately jump into an even larger thing. I wanted to make something small, something self-contained, something I could do entirely on my own. (My spouse is the better artist by far, and they did all the art for Cherry Kisses.)</p>
<p>The <span class="caps">PICO</span>-8 came to mind as the obvious platform to use. For one, the limitations make it very difficult for a game’s scope to balloon very far; you will simply run out of space and <em>have</em> to cut some ideas. For two, the art and audio are fairly low-resolution, so I wouldn’t have much opportunity to endlessly fuss over trying to make them perfect. For three, it runs in a browser, even on phones, so the resulting game would be easy for anyone to play. (Having to download a thing will discourage a surprising amount of casual passersby, especially if the thing is fairly small and thus low-reward.)</p>
<aside class="aside--well-actually">
<p>It's possible to make a PICO-8 game that spans multiple cartridges, so a game could be arbitrarily large! But I've never tried it and am never going to, because having a hard cap on how much stuff I can do is much of the appeal of PICO-8 for me.</p>
</aside>
<p>I also just find the <span class="caps">PICO</span>-8 endlessly charming, and I hadn’t touched it in a couple years and was curious how it had improved in the interim. It’s great for a game started on a whim, too, since I can jump in and start slapping stuff on the screen without worrying that my <span class="caps">ADHD</span> brain will start fretting over how everything should be organized.</p>
<p>That only left the question of <em>what</em> to make.</p>
<p>Two and a half years prior — almost three, now — I’d started on a platformer where you played as Star Anise, my cat’s fursona. It was intended to be a goofy Metroidvania where you collected cat-themed powers, ran around defeating little monsters, collected useless garbage, and generally left a trail of minor mayhem in your wake. Sadly, it was interrupted by real-life events and we haven’t touched it since.</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/gamedev-from-scratch/star-anise-original-sample.gif" alt="A clip of a pastel game where a small cat meows loudly and shoots a bubble gun that knocks jars off of shelves.">
</div>
<p>I loved how this game was shaping up! It was so goofy, but its goofiness really opened up the design. Star Anise is great to build a game around. I can give him all manner of strong yet absurd motivations, and as long as I tie them to something vaguely cat-themed, they’ll be memorable and feel sensible. I can load him up with goofy cat-themed powers without needing any kind of justification, because he’s a cat, and everyone knows cats are basically magic anyway. He has a group of friends already built in: other cats. And most importantly, he’s just fun to play as, because everything he does is ridiculous and overboard, but you never have to feel guilty about his mischief because he’s a cat.</p>
<p>It’s such a good hook. I’ve wanted to make a whole series of little Star Anise games, but the furthest I’d gotten so far was <a href="proxy.php?url=https://eevee.itch.io/anise-escape-despair">Star Anise Chronicles: Escape from the Chamber of Despair</a> — which is good, but is also a text adventure, one of the most impenetrable genres imaginable.</p>
<p>So why not take another crack at it? I couldn’t fit the entire original vision into a <span class="caps">PICO</span>-8 game, but surely I’d have enough room for Star Anise, a few of the abilities we’d come up with, and some things to interact with. At long last, a Star Anise platformer.</p>
<p>You could say the stars aligned. The stars. Get it? Like Star Anise. Okay.</p>
<h2 id="from-zero-to-something"><a class="toclink" href="proxy.php?url=#from-zero-to-something">From zero to something</a></h2>
<p>Before I could do anything, I needed some art. Okay, that’s not true; I could have boxes moving around on the screen, but I’ve done this enough that I am beyond tired of boxes. If I’m gonna make a Star Anise game then I want to have Star Anise on the damn screen right from the start.</p>
<p>And right away I had to make some decisions. I wanted this to be a <em>little bit</em> Metroidvania style, where Star Anise gained his handful of powers throughout the game and could then explore new areas.</p>
<p>That meant I wanted as much map space as humanly possible, so from the very beginning I knew the sprite/map split I wanted: all map. 32 screens, but only 128 sprites.</p>
<p>And that made several other decisions, automatically. I probably wouldn’t have enough sprite space to include a gun and enemies and whatnot, but a puzzler would let me skip all of that.</p>
<p>This is why I chose <span class="caps">PICO</span>-8! The game basically decided its own design with only minimal input from me. Puzzle platformer with some powerups.</p>
<p>Now, to draw Star Anise, which meant deciding how big he should be. A very conspicuous part of his design is his huge helmet, which wouldn’t fit especially well in a single 8×8 tile, or even in two of them stacked. I decided to go one bigger and make a 2×3 block.</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/gamedev-from-scratch/v1-sprite.png" alt="A charming little Star Anise sprite, with some extra bits next to him">
</div>
<p>This wasn’t especially complicated to draw. At this size, it feels like a lot of the sprites draw themselves, too. It did help that I’d already seen my spouse’s interpretation of Star Anise from the prototype game above, but I think the general lesson there is to look at existing art that’s similar to what you want to draw and reverse-engineer the bits that make it work. Here, I made a big circle, squeezed in the narrowest possible face — a pixel each for the eyes, then three pixels for spacing — and gave him a rectangle for his body. Toss a couple stars into the inside of the helmet and, presto, that’s Star Anise.</p>
<p>You might be wondering about those weird extra tiles on the side! I’ll get to those in a moment.</p>
<p>With Star Anise drawn, the obvious first thing is to put him on the dang screen.</p>
<div class="highlight"><pre><span></span><code><span class="linenos"> 1</span><span class="kr">function</span><span class="w"> </span><span class="nf">_init</span><span class="p">()</span>
<span class="linenos"> 2</span><span class="kr">end</span>
<span class="linenos"> 3</span>
<span class="linenos"> 4</span><span class="kr">function</span><span class="w"> </span><span class="nf">_update</span><span class="p">()</span>
<span class="linenos"> 5</span><span class="kr">end</span>
<span class="linenos"> 6</span>
<span class="linenos"> 7</span><span class="kr">function</span><span class="w"> </span><span class="nf">_draw</span><span class="p">()</span>
<span class="linenos"> 8</span><span class="w"> </span><span class="nf">cls</span><span class="p">()</span>
<span class="linenos"> 9</span><span class="w"> </span><span class="nf">spr</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">64</span><span class="p">,</span><span class="w"> </span><span class="mi">64</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="mi">3</span><span class="p">)</span>
<span class="linenos">10</span><span class="kr">end</span>
</code></pre></div>
<p>Some explanation may be in order. For starters, a “function” is a block of code that can be used repeatedly. (But then, this is not a programming tutorial.) These particular functions are special to the <span class="caps">PICO</span>-8: <code>_init</code> runs when the cartridge starts, <code>_update</code> runs every frame, and <code>_draw</code> also runs every frame.</p>
<p>What’s a frame, you ask? Well, you know how movies aren’t <em>really</em> showing movement, but are more like a very fast slideshow? Real life is “continuous” — that is, events occur smoothly over time, so when an object moves, it goes through every point between where it started and where it ends up. But we have no way to record that motion in full, becuase that would be an infinite amount of information! The best we can do is take a lot of snapshots very close together. And it turns out our eyes also work with snapshots (more or less), so it works well enough.</p>
<aside class="aside--well-actually">
<p>Arguably, real life might not be continuous, either. There's a thing called <em>Planck time</em>, the shortest possible meaningful unit of time. As I understand it, the idea is that all other amounts of time are just a (very large) number of Planck times, and the universe also exists as a series of snapshots.</p>
</aside>
<p>Likewise, <em>simulating</em> continuous behavior is extremely difficult, so video games tend to cheat the same way. We slice time into thin chunks — also called <em>frames</em> — and during each one, we move everything in the world ahead by that amount of time. If frames are short enough, you get the illusion that the world is behaving smoothly. Surprise! It’s all fake.</p>
<aside class="aside--computers-are-bad">
<p>The use of the word “frame” becomes incredibly confusing when dealing with sprite animation, which is also composed of frames (in the movie sense), but those frames may last varying amounts of time, including for multiple frames (in the game sense). You sometimes hear awful phrases like “a three-frame frame” or something. I am sorry on behalf of all programmers, who cannot help but reuse words in this way.</p>
</aside>
<p>Modern games can (or should) deal with a varying <em>frame rate</em>, where each frame is a slightly (or greatly) different duration for any of myriad reasons. Since the <span class="caps">PICO</span>-8 is a faux-retro console, I’ll be using the retro term <em>tic</em>. It means the same thing, but it’s sometimes used for older systems where the framerate is reliably fixed, usually because it’s tied to (or even enforced by) hardware somewhere. Here it’s just emulated, but, you know, close enough.</p>
<p>Right, so, back to the <span class="caps">PICO</span>-8 itself. Every tic (of which there are 30 per second), the <span class="caps">PICO</span>-8 does two things: it calls <code>_update</code> to advance the game, then it calls <code>_draw</code> to draw the new state of the game to the screen. You might immediately wonder: why have these be separate if they happen one after the other anyway? Great question! The answer is that the <span class="caps">PICO</span>-8 does something clever — if it notices that the <code>_update</code> + <code>_draw</code> combination is taking longer than one tic (and the game is thus starting to lag), it will automatically drop down to 15 <span class="caps">FPS</span>. In this mode, it will call <code>_update</code> <em>twice</em> and then call <code>_draw</code>. Here is a terrible <span class="caps">ASCII</span> diagram.</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span> | tic | tic |
<span class="linenos">2</span>--------+-----------------------+-----------------------+
<span class="linenos">3</span>30 FPS: | _update() _draw() | _update() _draw() |
<span class="linenos">4</span>--------+-----------------------+-----------------------+
<span class="linenos">5</span>15 FPS: | _update() _update() _draw() |
</code></pre></div>
<p>As you can see, the game still updates twice in the same amount of time, so it still <em>runs</em> at the same speed, but it only draws half as often. With any luck, that saves enough effort that the game can keep running at the intended speed.</p>
<p>All of that is to say: the <code>_draw</code> function draws to the screen.</p>
<p>The first thing you (<em>usually</em>) want to do in <code>_draw</code> is clear the screen, which is accomplished by the charmingly terse <code>cls()</code>. If you don’t do this, your game will merrily draw right on top of whatever was on the screen previously: the prompt, a previous game, even the code editor.</p>
<p>After that, I called <code>spr()</code> to draw Star Anise. The usual arguments are <code>spr(n, x, y)</code>, where <code>n</code> is the sprite number (visible near the middle of the screen in the sprite editor) and <code>x, y</code> say where to place him. He’s made up of six tiles, and you might think that drawing six tiles would thus require calling <code>spr()</code> six times, but it helpfully takes two more optional arguments: <em>how many</em> tiles to draw, as a single rectangle taken from the spritesheet. The above code thus draws a 2-by-3 block of tiles, starting from tile 1, at the coordinates (64, 64) — the center of the screen.</p>
<p>As is programming tradition, sprites are drawn from their top-left corner, so the initial tile is the top-left of the rectangle that gets drawn, and the coordinates are where the top-left of the drawn rectangle appears on screen. Thus, Star Anise appears with his top left “corner” in the middle of the screen.</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/gamedev-from-scratch/v1-anise.png" alt="Star Anise standing near the middle of the screen, as promised">
</div>
<p>There he is! How immensely satisfying. I always try to get <em>something</em> “real” drawing as early as humanly possible. It helps me feel like I’ve made some progress, like I’m working on a specific game and have made steps towards making it exist. This is already, quite clearly, a Star Anise game, but that wouldn’t be obvious if I’d started out with rectangles.</p>
<p>Now what? A good start would be to have him move around a bit. That’s easy enough if I introduce some state.</p>
<p>I do need to check what buttons the player is pressing, which I can do with <code>btn(b)</code>, where <code>b</code> is the button… number. Left is button 0, right is button 1, up is button 2… but that makes for some unreadable garbage, so instead, let’s use a recently-introduced shortcut. If you hold <kbd>Shift</kbd> and press <kbd>U</kbd>, <kbd>D</kbd>, <kbd>L</kbd>, <kbd>R</kbd>, <kbd>O</kbd>, or <kbd>X</kbd>, the <span class="caps">PICO</span>-8 will insert a symbol representing that button. (I will be representing those symbols as ⬆️⬇️⬅️➡️🅾️❎, which is how the <span class="caps">PICO</span>-8 stores them on disk.)</p>
<p>That’s enough to move him around:</p>
<div class="highlight"><pre><span></span><code><span class="linenos"> 1</span><span class="kr">function</span><span class="w"> </span><span class="nf">_init</span><span class="p">()</span>
<span class="linenos"> 2</span><span class="kr">end</span>
<span class="linenos"> 3</span>
<span class="linenos"> 4</span><span class="kd">local</span><span class="w"> </span><span class="nv">px</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">64</span>
<span class="linenos"> 5</span><span class="kd">local</span><span class="w"> </span><span class="nv">py</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">64</span>
<span class="linenos"> 6</span>
<span class="linenos"> 7</span><span class="kr">function</span><span class="w"> </span><span class="nf">_update</span><span class="p">()</span>
<span class="linenos"> 8</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nf">btn</span><span class="p">(</span><span class="err">⬆️</span><span class="p">)</span><span class="w"> </span><span class="kr">then</span>
<span class="linenos"> 9</span><span class="w"> </span><span class="nv">py</span><span class="w"> </span><span class="o">-=</span><span class="w"> </span><span class="mi">1</span>
<span class="linenos">10</span><span class="w"> </span><span class="kr">end</span>
<span class="linenos">11</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nf">btn</span><span class="p">(</span><span class="err">⬇️</span><span class="p">)</span><span class="w"> </span><span class="kr">then</span>
<span class="linenos">12</span><span class="w"> </span><span class="nv">py</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="mi">1</span>
<span class="linenos">13</span><span class="w"> </span><span class="kr">end</span>
<span class="linenos">14</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nf">btn</span><span class="p">(</span><span class="err">⬅️</span><span class="p">)</span><span class="w"> </span><span class="kr">then</span>
<span class="linenos">15</span><span class="w"> </span><span class="nv">px</span><span class="w"> </span><span class="o">-=</span><span class="w"> </span><span class="mi">1</span>
<span class="linenos">16</span><span class="w"> </span><span class="kr">end</span>
<span class="linenos">17</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nf">btn</span><span class="p">(</span><span class="err">➡️</span><span class="p">)</span><span class="w"> </span><span class="kr">then</span>
<span class="linenos">18</span><span class="w"> </span><span class="nv">px</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="mi">1</span>
<span class="linenos">19</span><span class="w"> </span><span class="kr">end</span>
<span class="linenos">20</span><span class="kr">end</span>
<span class="linenos">21</span>
<span class="linenos">22</span><span class="kr">function</span><span class="w"> </span><span class="nf">_draw</span><span class="p">()</span>
<span class="linenos">23</span><span class="w"> </span><span class="nf">cls</span><span class="p">()</span>
<span class="linenos">24</span><span class="w"> </span><span class="nf">spr</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="nv">px</span><span class="p">,</span><span class="w"> </span><span class="nv">py</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="mi">3</span><span class="p">)</span>
<span class="linenos">25</span><span class="kr">end</span>
</code></pre></div>
<p>Here I’ve put his position (still anchored at his top-left) into some variables, and during <code>_update()</code> I update them. (If you’re familiar with Lua, you may balk at <code>+=</code> and <code>-=</code> — these are extensions added by <span class="caps">PICO</span>-8, and they save enough space that they’re definitely worth it.)</p>
<aside class="aside--look-out">
<p>This code technically has a bug already. It looks like Star Anise is meant to move at 1 pixel per tic, but if you hold two directions at once, he'll move 1 pixel horizontally <em>and</em> one pixel vertically, for a combined velocity of <span class="math">\(\sqrt{2} \approx 1.4\)</span> pixels per tic. That's 40% faster than intended!</p>
<p>This is a common mistake (and a crucial <a href="proxy.php?url=https://doomwiki.org/wiki/Straferunning">speedrunning technique in classic Doom</a>), but it doesn't matter here — remember, I'm making a <em>platformer</em>, so the vertical movement will go away as soon as I have gravity and something for him to stand on.</p>
</aside>
<aside class="aside--look-out">
<p>There's <em>also</em> a problem with units here. Star Anise's velocity is 1 pixel/tic, a speed. But <code>px</code> is a position, measured in plain pixels. You can't meaningfully add a speed to a position. What's happening here is that this code runs once per tic, so it's implicitly multiplying by 1 tic to get 1 pixel, then adding that.</p>
<p>For any other game system, where fractions are easier to deal with and the framerate isn't fixed in stone, I'd probably measure speed in pixels <em>per second</em> (which is much easier to reason about) and have access to a <code>dt</code> value indicating how much time has passed since last frame. Then I could multiply those to get the distance travelled. Here, though, my life is easier if the numbers are integers, and multiplying by time would mean tacking on <code>* 1</code>. That eats valuable tokens and is more likely to add confusion than clarity, so I've left it off.</p>
</aside>
<aside class="aside--computers-are-bad">
<p>It feels a bit weird to initialize stuff <em>outside</em> of <code>_init</code>, the special PICO-8 function that runs when the cartridge first starts. (Code outside a function also runs when the cartridge starts. Earlier, in fact! It <em>has</em> to, because the <code>function _init()</code> block is what creates the <code>_init</code> function in the first place.)</p>
<p>The reason is, mostly, <code>local</code>. Lua's <code>local</code> keyword makes a variable that only exists within the block; without it, variables are <em>global</em> and exist everywhere in your entire program. But if I used it within <code>_init</code>, then <code>px</code> and <code>py</code> would only exist... within <code>_init</code>. I'd have to declare them with <code>local</code> outside and then assign them inside, and at that point I'm repeating myself for no reason.</p>
<p>So why use it at all, for globals? Three reasons. One, it's a good habit to get into with Lua so you don't accidentally make globals in the wrong place. Two, it clearly marks where a variable was intended to be <em>created</em>; if I only wrote <code>px = 64</code>, that could also mean I think I'm setting an <em>existing</em> variable called <code>px</code>, and using <code>local</code> avoids that ambiguity. And three, it's very slightly faster for complicated reasons.</p>
<p>That said, my insistence on <code>local</code> everywhere does create a couple small problems later on, since a <code>local</code> variable only exists from that line onwards.</p>
</aside>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/gamedev-from-scratch/v1-anise-moving.gif" alt="Star Anise sliding around the screen">
</div>
<p>This is already halfway to being a game — it does something when I press buttons! Excellent. But also weird. This doesn’t look like Star Anise is walking around; it looks like he’s a static image being dragged by an invisible cursor or something. A very easy aesthetic improvement would be to make him not moonwalk when moving left.</p>
<p>That’s easy enough; the <code>spr()</code> function takes two more optional arguments, indicating whether to flip the sprite horizontally and/or vertically. I can just slap those in when he’s moving left. Or, well, not <em>quite</em> — I want to flip him when the <em>last direction he moved</em> was left. If he moves left and then stops, or moves left and then up and down, he should still be facing left.</p>
<div class="highlight"><pre><span></span><code><span class="linenos"> 1</span><span class="kr">function</span><span class="w"> </span><span class="nf">_init</span><span class="p">()</span>
<span class="linenos"> 2</span><span class="kr">end</span>
<span class="linenos"> 3</span>
<span class="linenos"> 4</span><span class="kd">local</span><span class="w"> </span><span class="nv">px</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">64</span>
<span class="linenos"> 5</span><span class="kd">local</span><span class="w"> </span><span class="nv">py</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">64</span>
<span class="linenos"> 6</span><span class="kd">local</span><span class="w"> </span><span class="nv">left</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">false</span>
<span class="linenos"> 7</span>
<span class="linenos"> 8</span><span class="kr">function</span><span class="w"> </span><span class="nf">_update</span><span class="p">()</span>
<span class="linenos"> 9</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nf">btn</span><span class="p">(</span><span class="err">⬆️</span><span class="p">)</span><span class="w"> </span><span class="kr">then</span>
<span class="linenos">10</span><span class="w"> </span><span class="nv">py</span><span class="w"> </span><span class="o">-=</span><span class="w"> </span><span class="mi">1</span>
<span class="linenos">11</span><span class="w"> </span><span class="kr">end</span>
<span class="linenos">12</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nf">btn</span><span class="p">(</span><span class="err">⬇️</span><span class="p">)</span><span class="w"> </span><span class="kr">then</span>
<span class="linenos">13</span><span class="w"> </span><span class="nv">py</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="mi">1</span>
<span class="linenos">14</span><span class="w"> </span><span class="kr">end</span>
<span class="linenos">15</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nf">btn</span><span class="p">(</span><span class="err">⬅️</span><span class="p">)</span><span class="w"> </span><span class="kr">then</span>
<span class="linenos">16</span><span class="w"> </span><span class="nv">px</span><span class="w"> </span><span class="o">-=</span><span class="w"> </span><span class="mi">1</span>
<span class="linenos">17</span><span class="w"> </span><span class="nv">left</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">true</span>
<span class="linenos">18</span><span class="w"> </span><span class="kr">end</span>
<span class="linenos">19</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nf">btn</span><span class="p">(</span><span class="err">➡️</span><span class="p">)</span><span class="w"> </span><span class="kr">then</span>
<span class="linenos">20</span><span class="w"> </span><span class="nv">px</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="mi">1</span>
<span class="linenos">21</span><span class="w"> </span><span class="nv">left</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">false</span>
<span class="linenos">22</span><span class="w"> </span><span class="kr">end</span>
<span class="linenos">23</span><span class="kr">end</span>
<span class="linenos">24</span>
<span class="linenos">25</span><span class="kr">function</span><span class="w"> </span><span class="nf">_draw</span><span class="p">()</span>
<span class="linenos">26</span><span class="w"> </span><span class="nf">cls</span><span class="p">()</span>
<span class="linenos">27</span><span class="w"> </span><span class="nf">spr</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="nv">px</span><span class="p">,</span><span class="w"> </span><span class="nv">py</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span><span class="w"> </span><span class="nv">left</span><span class="p">)</span>
<span class="linenos">28</span><span class="kr">end</span>
</code></pre></div>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/gamedev-from-scratch/v1-anise-moving2.gif" alt="Star Anise sliding around the screen, but turning around when moving left">
</div>
<p>Making progress, but obviously he’d look a lot better if he were animated, right?</p>
<p>Which, finally, brings us back to those extra tiles I drew. They’re copies of Star Anise’s legs and antenna, lightly edited to look like he’s in mid-step. The legs are sticking out all the way, and the antenna is adjusted to be… positioned slightly differently, since it’s bouncy. It’s a bit rough, but I can touch it up later.</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/gamedev-from-scratch/v1-anise-walk.gif" alt="Star Anise's walk animation">
</div>
<p>Note that I’ve crammed as much movement into as little space as possible here. This is only a two-frame animation, so the leg movement is exaggerated to get the most bang for my buck. I don’t even duplicate the entirety of Star Anise for the other frame; instead, I only copied the tiles that change. That’ll make him more complicated to draw, but it does save me sprite space — remember, I only have 127 tiles available, and 9 of them is already 7% gone. (Writing more code to save on limited asset space is, in my experience, a pretty common <span class="caps">PICO</span>-8 tactic.)</p>
<p>Unfortunately, this makes flipping his sprite somewhat more complicated. I can’t just use that argument to <code>spr()</code>, because— well, I’ll get to that in a second. Here’s the updated code.</p>
<div class="highlight"><pre><span></span><code><span class="linenos"> 1</span><span class="kd">local</span><span class="w"> </span><span class="nv">anise_stand</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="mi">17</span><span class="p">,</span><span class="w"> </span><span class="mi">18</span><span class="p">,</span><span class="w"> </span><span class="mi">33</span><span class="p">,</span><span class="w"> </span><span class="mi">34</span><span class="p">}</span>
<span class="linenos"> 2</span><span class="kd">local</span><span class="w"> </span><span class="nv">anise_jump</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="mi">3</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="mi">17</span><span class="p">,</span><span class="w"> </span><span class="mi">18</span><span class="p">,</span><span class="w"> </span><span class="mi">19</span><span class="p">,</span><span class="w"> </span><span class="mi">35</span><span class="p">}</span>
<span class="linenos"> 3</span>
<span class="linenos"> 4</span><span class="kr">function</span><span class="w"> </span><span class="nf">_init</span><span class="p">()</span>
<span class="linenos"> 5</span><span class="kr">end</span>
<span class="linenos"> 6</span>
<span class="linenos"> 7</span><span class="kd">local</span><span class="w"> </span><span class="nv">t</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span>
<span class="linenos"> 8</span><span class="kd">local</span><span class="w"> </span><span class="nv">px</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">64</span>
<span class="linenos"> 9</span><span class="kd">local</span><span class="w"> </span><span class="nv">py</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">64</span>
<span class="linenos">10</span><span class="kd">local</span><span class="w"> </span><span class="nv">left</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">false</span>
<span class="linenos">11</span><span class="kd">local</span><span class="w"> </span><span class="nv">moving</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">false</span>
<span class="linenos">12</span>
<span class="linenos">13</span><span class="kr">function</span><span class="w"> </span><span class="nf">_update</span><span class="p">()</span>
<span class="linenos">14</span><span class="w"> </span><span class="nv">t</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="mi">1</span>
<span class="linenos">15</span><span class="w"> </span><span class="nv">t</span><span class="w"> </span><span class="o">%=</span><span class="w"> </span><span class="mi">120</span>
<span class="linenos">16</span>
<span class="linenos">17</span><span class="w"> </span><span class="nv">moving</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">false</span>
<span class="linenos">18</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nf">btn</span><span class="p">(</span><span class="err">⬆️</span><span class="p">)</span><span class="w"> </span><span class="kr">then</span>
<span class="linenos">19</span><span class="w"> </span><span class="nv">py</span><span class="w"> </span><span class="o">-=</span><span class="w"> </span><span class="mi">1</span>
<span class="linenos">20</span><span class="w"> </span><span class="nv">moving</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">true</span>
<span class="linenos">21</span><span class="w"> </span><span class="kr">end</span>
<span class="linenos">22</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nf">btn</span><span class="p">(</span><span class="err">⬇️</span><span class="p">)</span><span class="w"> </span><span class="kr">then</span>
<span class="linenos">23</span><span class="w"> </span><span class="nv">py</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="mi">1</span>
<span class="linenos">24</span><span class="w"> </span><span class="nv">moving</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">true</span>
<span class="linenos">25</span><span class="w"> </span><span class="kr">end</span>
<span class="linenos">26</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nf">btn</span><span class="p">(</span><span class="err">⬅️</span><span class="p">)</span><span class="w"> </span><span class="kr">then</span>
<span class="linenos">27</span><span class="w"> </span><span class="nv">px</span><span class="w"> </span><span class="o">-=</span><span class="w"> </span><span class="mi">1</span>
<span class="linenos">28</span><span class="w"> </span><span class="nv">moving</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">true</span>
<span class="linenos">29</span><span class="w"> </span><span class="nv">left</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">true</span>
<span class="linenos">30</span><span class="w"> </span><span class="kr">end</span>
<span class="linenos">31</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nf">btn</span><span class="p">(</span><span class="err">➡️</span><span class="p">)</span><span class="w"> </span><span class="kr">then</span>
<span class="linenos">32</span><span class="w"> </span><span class="nv">px</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="mi">1</span>
<span class="linenos">33</span><span class="w"> </span><span class="nv">moving</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">true</span>
<span class="linenos">34</span><span class="w"> </span><span class="nv">left</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">false</span>
<span class="linenos">35</span><span class="w"> </span><span class="kr">end</span>
<span class="linenos">36</span><span class="kr">end</span>
<span class="linenos">37</span>
<span class="linenos">38</span><span class="kr">function</span><span class="w"> </span><span class="nf">_draw</span><span class="p">()</span>
<span class="linenos">39</span><span class="w"> </span><span class="nf">cls</span><span class="p">()</span>
<span class="linenos">40</span>
<span class="linenos">41</span><span class="w"> </span><span class="kd">local</span><span class="w"> </span><span class="nv">pose</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">anise_stand</span>
<span class="linenos">42</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">moving</span><span class="w"> </span><span class="ow">and</span><span class="w"> </span><span class="nv">t</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="mi">8</span><span class="w"> </span><span class="o"><</span><span class="w"> </span><span class="mi">4</span><span class="w"> </span><span class="kr">then</span>
<span class="linenos">43</span><span class="w"> </span><span class="nv">pose</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">anise_jump</span>
<span class="linenos">44</span><span class="w"> </span><span class="kr">end</span>
<span class="linenos">45</span><span class="w"> </span><span class="kd">local</span><span class="w"> </span><span class="nv">y</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">py</span>
<span class="linenos">46</span><span class="w"> </span><span class="kd">local</span><span class="w"> </span><span class="nv">x0</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">px</span>
<span class="linenos">47</span><span class="w"> </span><span class="kd">local</span><span class="w"> </span><span class="nv">dx</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">8</span>
<span class="linenos">48</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">left</span><span class="w"> </span><span class="kr">then</span>
<span class="linenos">49</span><span class="w"> </span><span class="nv">dx</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">-</span><span class="mi">8</span>
<span class="linenos">50</span><span class="w"> </span><span class="nv">x0</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="mi">8</span>
<span class="linenos">51</span><span class="w"> </span><span class="kr">end</span>
<span class="linenos">52</span><span class="w"> </span><span class="kd">local</span><span class="w"> </span><span class="nv">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">x0</span>
<span class="linenos">53</span><span class="w"> </span><span class="kr">for</span><span class="w"> </span><span class="nv">i</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="o">#</span><span class="nv">pose</span><span class="w"> </span><span class="kr">do</span>
<span class="linenos">54</span><span class="w"> </span><span class="nf">spr</span><span class="p">(</span><span class="nv">pose</span><span class="p">[</span><span class="nv">i</span><span class="p">],</span><span class="w"> </span><span class="nv">x</span><span class="p">,</span><span class="w"> </span><span class="nv">y</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="nv">left</span><span class="p">)</span>
<span class="linenos">55</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">i</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="kr">then</span>
<span class="linenos">56</span><span class="w"> </span><span class="nv">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">x0</span>
<span class="linenos">57</span><span class="w"> </span><span class="nv">y</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="mi">8</span>
<span class="linenos">58</span><span class="w"> </span><span class="kr">else</span>
<span class="linenos">59</span><span class="w"> </span><span class="nv">x</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="nv">dx</span>
<span class="linenos">60</span><span class="w"> </span><span class="kr">end</span>
<span class="linenos">61</span><span class="w"> </span><span class="kr">end</span>
<span class="linenos">62</span><span class="kr">end</span>
</code></pre></div>
<p>That sure got longer in a hurry! A quick overview:</p>
<p>I’ve introduced a global called <code>t</code> to act as a clock. I intend to use this for animation and other global cycles, so I don’t care about the <em>actual</em> time — that’s why I take it mod 120.</p>
<p>If you’re not familiar, the <code>%</code> (or “modulus”) operator gives you the remainder after division. It’s super duper useful and I wish we taught it as a primitive math operation! You can think of it like “clock arithmetic” — if it’s 9 o’clock and you wait 4 hours, it becomes 1 o’clock, which is the remainder when you divide 9 + 4 by 12. Or you can think of it as removing all chunks of something — to convert the 24-hour “13 o’clock” to 12-hour, you remove all the 12s, leaving just 1 behind. Or you can think of it as coiling the entire number line into a circle, so after 11 you wrap around to 0 and start over. (That’s not quite how clocks work, but using 0–11 turns out to be much simpler than using 1–12.)</p>
<p>The upshot here is that <code>t</code> will hit 119 and then wrap back around to zero, which is important because <span class="caps">PICO</span>-8 numbers can’t go any higher than 32767. If I left it to its own devices, it would still wrap around, but to the more cumbersome -32768. I don’t want a negative clock!</p>
<p>But why 120? Because I want to be able to divide the clock cycle into smaller animation cycles, and I can only do that evenly if the whole clock’s length is a multiple of the smaller cycle’s length. (On a more powerful system, I’d have a more elaborate animation setup, but that would cost more space and code than I’m willing to spend here.) Consider if I had a clock that wrapped around at 10, and I wanted an animation 3 tics long. I would use modulo 3 to shrink the clock, resulting in:</p>
<div class="prose-full-illustration">
<object type="image/svg+xml" data="https://eev.ee/media/gamedev-from-scratch/v1-clock-cycle.svg"></object>
</div>
<p>Whoops! Frame 0 will show twice in a row, intermittently, even seemingly at random. That’s not great. For the best chance of avoiding that problem without having to think too hard about it, I want a clock whose length is divisible by as much stuff as possible — a <a href="proxy.php?url=https://en.wikipedia.org/wiki/Highly_composite_number">highly composite number</a>. And, of course, 120 is one such number.</p>
<aside class="aside--note-from-future">
<p>It's not an especially good such number, though; it's not even divisible by 7! I have everything up to 32767 to choose from, so later I will switch to a clock length of 20160. Not really sure why I don't go with 27720, which is the biggest highly composite number that the PICO-8 can express.</p>
<p>Oh, and after a few rounds of also using <code>t</code> as a variable name for a tile id, I'll rename the clock to the much more intuitive... <code>clock</code>.</p>
</aside>
<p>Next, I track whether Star Anise is moving <em>at all</em>, so I know whether to play the walk animation. Note that I always assume he <em>isn’t</em> moving, and then correct myself if it turns out he is; otherwise, the new value of <code>moving</code> would persist into future tics and he’d never stop.</p>
<p>That brings me to the new drawing code, which is a little tricky, so here it is a bit at a time:</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="c1">-- top of the file</span>
<span class="linenos">2</span><span class="kd">local</span><span class="w"> </span><span class="nv">anise_stand</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="mi">17</span><span class="p">,</span><span class="w"> </span><span class="mi">18</span><span class="p">,</span><span class="w"> </span><span class="mi">33</span><span class="p">,</span><span class="w"> </span><span class="mi">34</span><span class="p">}</span>
<span class="linenos">3</span><span class="kd">local</span><span class="w"> </span><span class="nv">anise_jump</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="mi">3</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="mi">17</span><span class="p">,</span><span class="w"> </span><span class="mi">18</span><span class="p">,</span><span class="w"> </span><span class="mi">19</span><span class="p">,</span><span class="w"> </span><span class="mi">35</span><span class="p">}</span>
<span class="linenos">4</span>
<span class="linenos">5</span><span class="w"> </span><span class="c1">-- in _draw()</span>
<span class="linenos">6</span><span class="w"> </span><span class="kd">local</span><span class="w"> </span><span class="nv">pose</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">anise_stand</span>
<span class="linenos">7</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">moving</span><span class="w"> </span><span class="ow">and</span><span class="w"> </span><span class="nv">t</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="mi">8</span><span class="w"> </span><span class="o"><</span><span class="w"> </span><span class="mi">4</span><span class="w"> </span><span class="kr">then</span>
<span class="linenos">8</span><span class="w"> </span><span class="nv">pose</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">anise_jump</span>
<span class="linenos">9</span><span class="w"> </span><span class="kr">end</span>
</code></pre></div>
<p>This decides which tiles I’m going to draw. I can’t draw the walking part (which I’ve called “jump” because it does look like a jump in isolation, and I’ll be reusing them for that later) as a single block with <code>spr()</code> like before, and I’d like to share the code, so both frames are now assembled from individual tiles.</p>
<p>Note that tiles 1, 2, 17, 18, 33, and 34 are exactly the ones I was drawing in a single <code>spr()</code> call before. (The numbers increase by 16 when jumping to the next row, which makes sense, because each row has 16 tiles in it.) The other set is similar, but it has the alternate tiles substituted in.</p>
<p>I only want to use the jump tiles if Star Anise is moving, <em>and</em> if <code>t % 8 < 4</code>. That <code>%</code> turns my 120-tic clock into an 8-tic clock, then checks if we’re in the first half of it. Essentially: if it’s before noon, show the alternate frame; otherwise, show the normal standing frame.</p>
<div class="prose-full-illustration">
<object type="image/svg+xml" data="https://eev.ee/media/gamedev-from-scratch/v1-clock-animation.svg"></object>
</div>
<p>The use of a global timer does have some subtle drawbacks here. If I tap an arrow key to move Star Anise only very briefly, then he may or may not animate, depending on whether the tap happens to be during the “stand” or “jump” intervals. A more powerful system, where every animation kept track of its own time, would always briefly show him moving. (On the other hand, this is an interesting aesthetic in its own right that kinda complements the very low-res and exaggerated animation.)</p>
<p>Next I need to draw the tiles, but we’ve come to the catch I mentioned before. When I draw Star Anise flipped, I’m now drawing him as a bunch of separate tiles. If I drew them in the same left-to-right order, then his left side would be flipped, and his right side would be flipped, but the <em>whole image</em> wouldn’t be. Er, just look at this picture.</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/gamedev-from-scratch/v1-anise-walk-naive-flipped.png" alt="Star Anise's walk frames, flipped one tile at a time">
</div>
<p>See? The tiles are arranged the same way, but each one is <em>individually</em> flipped, and the result is… not what I want. I’ll need to also draw the columns in reverse order. And that’s exactly what I do:</p>
<div class="highlight"><pre><span></span><code><span class="linenos">45</span><span class="w"> </span><span class="kd">local</span><span class="w"> </span><span class="nv">y</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">py</span>
<span class="linenos">46</span><span class="w"> </span><span class="kd">local</span><span class="w"> </span><span class="nv">x0</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">px</span>
<span class="linenos">47</span><span class="w"> </span><span class="kd">local</span><span class="w"> </span><span class="nv">dx</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">8</span>
<span class="linenos">48</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">left</span><span class="w"> </span><span class="kr">then</span>
<span class="linenos">49</span><span class="w"> </span><span class="nv">dx</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">-</span><span class="mi">8</span>
<span class="linenos">50</span><span class="w"> </span><span class="nv">x0</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="mi">8</span>
<span class="linenos">51</span><span class="w"> </span><span class="kr">end</span>
</code></pre></div>
<p>Here I’m determining the start point and how far apart the tiles are. The variable names are fairly terse, for a couple of reasons: one, the <span class="caps">PICO</span>-8 screen is not very wide, so long variable names make code much harder to read; but also, math code tends to be easier to follow with shorter names anyway. I’ve even taken the naming conventions from math — the initial state of a variable is often written with a subscript zero (<span class="math">\(x_0\)</span>) and a change is written with the Greek letter delta (<span class="math">\(\Delta x\)</span>), so I’ve used the <span class="caps">ASCII</span> equivalents of those, <code>x0</code> and <code>dx</code>.</p>
<p>I’m starting from Star Anise’s position, of course, and then each tile is 8 pixels right of the previous one… if he’s not flipped. If he <em>is</em> flipped, I want to move <em>left</em>, which will draw the tiles in reverse order. But that would change where he draws from, so to compensate, I also start drawing 8 pixels right of where I usually would. (Try to convince yourself that this is correct; on a flipped Star Anise, tile number 1 should draw 8 pixels left from his upper-left corner.)</p>
<div class="highlight"><pre><span></span><code><span class="linenos">52</span><span class="w"> </span><span class="kd">local</span><span class="w"> </span><span class="nv">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">x0</span>
<span class="linenos">53</span><span class="w"> </span><span class="kr">for</span><span class="w"> </span><span class="nv">i</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="o">#</span><span class="nv">pose</span><span class="w"> </span><span class="kr">do</span>
<span class="linenos">54</span><span class="w"> </span><span class="nf">spr</span><span class="p">(</span><span class="nv">pose</span><span class="p">[</span><span class="nv">i</span><span class="p">],</span><span class="w"> </span><span class="nv">x</span><span class="p">,</span><span class="w"> </span><span class="nv">y</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="nv">left</span><span class="p">)</span>
<span class="linenos">55</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">i</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="kr">then</span>
<span class="linenos">56</span><span class="w"> </span><span class="nv">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">x0</span>
<span class="linenos">57</span><span class="w"> </span><span class="nv">y</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="mi">8</span>
<span class="linenos">58</span><span class="w"> </span><span class="kr">else</span>
<span class="linenos">59</span><span class="w"> </span><span class="nv">x</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="nv">dx</span>
<span class="linenos">60</span><span class="w"> </span><span class="kr">end</span>
<span class="linenos">61</span><span class="w"> </span><span class="kr">end</span>
</code></pre></div>
<p>All that’s left to do is the drawing itself. For each tile in the <code>pose</code> list, I draw that tile. Each row is two tiles wide, so after every second tile, I reset the horizontal “cursor” (<code>x</code>) back to where it started and move down by one row’s worth of pixels. For any other tile, I just move horizontally by <code>dx</code>.</p>
<p>The results are basically magic.</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/gamedev-from-scratch/v1-anise-moving3.gif" alt="Star Anise walking around the screen and turning to face the way he's moving">
</div>
<p>And that’s a good place to pause for now. Yes, I know, we didn’t get very far, but this <em>is</em> part zero! It’s mostly a test of this series and its tone for me, and a test of fortitude for you. I hope you could follow along with the minor mathematical hijinks above, because next time it gets <em>much</em> worse — before I can do anything else at all, I have to write <em>collision detection</em>. Oh boy! Stay tuned! And always feel free to ask questions, of me or anyone else!</p>
<p><strong><a href="proxy.php?url=https://eev.ee/blog/2021/01/26/gamedev-from-scratch-1-scaffolding/">Part 1: Scaffolding →</a></strong></p>
<h2 id="appendix-pico-8-lua-extensions"><a class="toclink" href="proxy.php?url=#appendix-pico-8-lua-extensions">Appendix: PICO-8 Lua extensions</a></h2>
<p>Here are all the modifications <span class="caps">PICO</span>-8 has made to the language (based on Lua 5.2). If you’ve never used Lua, keep in mind that these won’t carry over if you try to write Lua anywhere else. Some of these are advanced features, so if you have no idea what something means, that’s probably fine.</p>
<p>Spoilers: it’s mostly that the standard library has changed.</p>
<ul>
<li>
<p>Numbers are signed 15.16 fixed-point, rather than stock Lua’s 64-bit floating point. That means fractions can only be represented in increments of 0.0000152587890625 (= <span class="math">\(2^{-16}\)</span>, a cumbersome number I refer to as the “Planck size”), and numbers can’t exceed ±32768.</p>
</li>
<li>
<p>Compound assignment is supported: <code>a += b</code> works as in <code>a = a + b</code> in stock Lua, where <code>+</code> can be replaced with any binary operator.</p>
</li>
<li>
<p><code>!=</code> is allowed as an alias for <code>~=</code>.</p>
</li>
<li>
<p><code>if (foo) bar = 1</code> is shorthand for <code>if foo then bar = 1 end</code>. The parentheses are required, and the condition ends at the end of the line. (I strongly advise against using this unless you’re very desperate for space; it scans poorly and doesn’t even save tokens.)</p>
</li>
<li>
<p>The new <code>@</code>, <code>%</code>, and <code>$</code> unary prefix operators read 1, 2, or 4 bytes from a memory address. (<span class="caps">PICO</span>-8’s memory, not system <span class="caps">RAM</span>!)</p>
</li>
<li>
<p>The <code>?</code> unary prefix operator is equivalent to <code>print</code>. (I’ve never used it, and it’s not even directly documented.)</p>
</li>
<li>
<p>The built-in functions <code>collectgarbage</code>, <code>dofile</code>, <code>error</code>, <code>pcall</code>, <code>require</code>, <code>select</code>, and <code>xpcall</code> are not available (though the lack of <code>select</code> might be a bug).</p>
<p>The built-in variables <code>_G</code> and <code>_VERSION</code> are not available.</p>
<p><code>load</code> has been replaced with a function that loads <span class="caps">PICO</span>-8 carts from files.</p>
<p><code>print</code> has been replaced with a drawing function, which prints a single string at a position on screen.</p>
<p><code>tonumber</code> and <code>tostring</code> have been replaced with <code>tonum</code> and <code>tostr</code>, which behave slightly differently (but <code>tostr</code> does still respect the <code>__tostring</code> metatable field).</p>
<p>(<code>assert</code>, <code>getmetatable</code>, <code>ipairs</code>, <code>next</code>, <code>pairs</code>, <code>rawequal</code>, <code>rawget</code>, <code>rawlen</code>, <code>rawset</code>, <code>setmetatable</code>, and <code>type</code> still exist and work as in stock Lua.)</p>
</li>
<li>
<p>The <code>coroutine</code> library is not available, but most of its contents are exposed directly as <code>cocreate</code>, <code>coresume</code>, <code>costatus</code>, and <code>yield</code>. There is no equivalent for <code>coroutine.running</code> or <code>coroutine.wrap</code>.</p>
</li>
<li>
<p>The <code>require</code> function and <code>package</code> library are not available, though the <code>#include</code> syntax can be used to textually substitute the contents of a Lua file.</p>
</li>
<li>
<p>The <code>string</code> library is not available. Replacement string functions are: <code>chr</code>, <code>ord</code>, <code>split</code>, and <code>sub</code>.</p>
</li>
<li>
<p>The <code>table</code> library is not available. Replacement table functions are: <code>add</code>, <code>del</code>, <code>deli</code>, <code>count</code>, <code>all</code>, <code>foreach</code>. There is no built-in way to concatenate or sort a list.</p>
</li>
<li>
<p>The <code>math</code> library is not available. Replacement math functions are: <code>max</code>, <code>min</code>, <code>mid</code>, <code>flr</code>, <code>ceil</code>, <code>sin</code>, <code>cos</code>, <code>atan2</code>, <code>sqrt</code>, <code>abs</code>, <code>rnd</code>, <code>srand</code>. There is also an integer division operator, <code>\</code>.</p>
</li>
<li>
<p>The <code>bit32</code> library is not available, but bitwise operations are available as both functions — <code>band</code>, <code>bor</code>, <code>bxor</code>, <code>bnot</code>, <code>shl</code>, <code>shr</code>, <code>lshr</code>, <code>rotl</code>, <code>rotr</code> — and operators — <code>&</code>, <code>|</code>, <code>^^</code>, <code>~</code>, <code><<</code>, <code>>></code>, <code>>>></code>, <code><<></code>, <code>>><</code>.</p>
</li>
<li>
<p>The <code>io</code> library is not available. Running <span class="caps">PICO</span>-8 cartridges have no notion of a filesystem.</p>
</li>
<li>
<p>The <code>os</code> library is not available. Running <span class="caps">PICO</span>-8 cartridges have no direct access to the underlying operating system. (Some facilities are exposed through the “syscall” function <code>stat</code>, such as accessing the current <span class="caps">UTC</span> or local time.)</p>
</li>
<li>
<p>The <code>debug</code> library is not available.</p>
</li>
<li>
<p>A number of other new functions were added, though I won’t list them all here; they’re generally for drawing, working with assets, or interacting with the <span class="caps">PICO</span>-8’s faux hardware.</p>
</li>
</ul>
<script type="text/javascript">if (!document.getElementById('mathjaxscript_pelican_#%@#$@#')) {
var align = "center",
indent = "0em",
linebreak = "false";
if (false) {
align = (screen.width < 768) ? "left" : align;
indent = (screen.width < 768) ? "0em" : indent;
linebreak = (screen.width < 768) ? 'true' : linebreak;
}
var mathjaxscript = document.createElement('script');
mathjaxscript.id = 'mathjaxscript_pelican_#%@#$@#';
mathjaxscript.type = 'text/javascript';
mathjaxscript.src = 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.3/latest.js?config=TeX-AMS-MML_HTMLorMML';
var configscript = document.createElement('script');
configscript.type = 'text/x-mathjax-config';
configscript[(window.opera ? "innerHTML" : "text")] =
"MathJax.Hub.Config({" +
" config: ['MMLorHTML.js']," +
" TeX: { extensions: ['AMSmath.js','AMSsymbols.js','noErrors.js','noUndefined.js'], equationNumbers: { autoNumber: 'none' } }," +
" jax: ['input/TeX','input/MathML','output/HTML-CSS']," +
" extensions: ['tex2jax.js','mml2jax.js','MathMenu.js','MathZoom.js']," +
" displayAlign: '"+ align +"'," +
" displayIndent: '"+ indent +"'," +
" showMathMenu: true," +
" messageStyle: 'normal'," +
" tex2jax: { " +
" inlineMath: [ ['\\\\(','\\\\)'] ], " +
" displayMath: [ ['$$','$$'] ]," +
" processEscapes: true," +
" preview: 'TeX'," +
" }, " +
" 'HTML-CSS': { " +
" availableFonts: ['STIX', 'TeX']," +
" preferredFont: 'STIX'," +
" styles: { '.MathJax_Display, .MathJax .mo, .MathJax .mi, .MathJax .mn': {color: 'inherit ! important'} }," +
" linebreaks: { automatic: "+ linebreak +", width: '90% container' }," +
" }, " +
"}); " +
"if ('default' !== 'default') {" +
"MathJax.Hub.Register.StartupHook('HTML-CSS Jax Ready',function () {" +
"var VARIANT = MathJax.OutputJax['HTML-CSS'].FONTDATA.VARIANT;" +
"VARIANT['normal'].fonts.unshift('MathJax_default');" +
"VARIANT['bold'].fonts.unshift('MathJax_default-bold');" +
"VARIANT['italic'].fonts.unshift('MathJax_default-italic');" +
"VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');" +
"});" +
"MathJax.Hub.Register.StartupHook('SVG Jax Ready',function () {" +
"var VARIANT = MathJax.OutputJax.SVG.FONTDATA.VARIANT;" +
"VARIANT['normal'].fonts.unshift('MathJax_default');" +
"VARIANT['bold'].fonts.unshift('MathJax_default-bold');" +
"VARIANT['italic'].fonts.unshift('MathJax_default-italic');" +
"VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');" +
"});" +
"}";
(document.body || document.getElementsByTagName('head')[0]).appendChild(configscript);
(document.body || document.getElementsByTagName('head')[0]).appendChild(mathjaxscript);
}
</script>Lexy’s Labyrinth2020-09-26T19:28:00-07:002020-09-26T19:28:00-07:00Eeveetag:eev.ee,2020-09-26:/blog/2020/09/26/lexys-labyrinth/<div class="prose-full-illustration">
<img alt="Screenshot of a small tile-based puzzle with a number of different elements, taken from CCLP1" src="proxy.php?url=https://eev.ee/media/updates/lexys-labyrinth/lexys-labyrinth.png"/>
</div>
<p>🔗 <a href="proxy.php?url=https://c.eev.ee/lexys-labyrinth/"><strong>Lexy’s Labyrinth</strong></a><br/>
🔗 <a href="proxy.php?url=https://github.com/eevee/lexys-labyrinth"><strong>Source code on GitHub</strong></a><br/>
🔗 itch.io later</p>
<p>Here is Lexy’s Labyrinth, a web-based Chip’s Challenge emulator.</p>
<p>It’s easy to get into and mostly speaks for itself, so here is a story.</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/updates/lexys-labyrinth/lexys-labyrinth.png" alt="Screenshot of a small tile-based puzzle with a number of different elements, taken from CCLP1">
</div>
<p>🔗 <a href="proxy.php?url=https://c.eev.ee/lexys-labyrinth/"><strong>Lexy’s Labyrinth</strong></a><br />
🔗 <a href="proxy.php?url=https://github.com/eevee/lexys-labyrinth"><strong>Source code on GitHub</strong></a><br />
🔗 itch.io later</p>
<p>Here is Lexy’s Labyrinth, a web-based Chip’s Challenge emulator.</p>
<p>It’s easy to get into and mostly speaks for itself, so here is a story.</p>
<hr />
<p>Once upon a time, there was a puzzle game called <a href="proxy.php?url=https://en.wikipedia.org/wiki/Chip%27s_Challenge">Chip’s Challenge</a>. It was created in 1989 for the Atari Lynx, an early handheld that is probably best known for… uh… Chip’s Challenge. It stood out as a curious blend of Sokoban head-scratching with real-time action, and it was one of the first computer puzzle games that had a whole pile of different mechanics and relied on exploiting the interesting interactions between them<sup>[citation needed]</sup>.</p>
<p>The game found wider recognition with its inclusion in <a href="proxy.php?url=https://en.wikipedia.org/wiki/Microsoft_Entertainment_Pack">Microsoft Entertainment Pack 4</a>, and later the Best of Windows Entertainment Pack (charmingly abbreviated “<span class="caps">BOWEP</span>”).</p>
<hr />
<p>That in itself is a curious story — numerous features of the Atari Lynx version were lost in translation, most notably that the Lynx version has the player and monsters slide smoothly between grid cells, whereas the Microsoft port has everything instantly snap from one cell to the next. Also conspicuous is the presence of several typos in level passwords, which are exactly consistent with <a href="proxy.php?url=https://wiki.bitbusters.club/Jaime_Villacorte%27s_notes">a set of notes a player took about the Lynx game</a>, but which would be impossible in a straight port — the Lynx level passwords weren’t manually set, but were generated on the fly by a <span class="caps">PRNG</span>.</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/updates/lexys-labyrinth/mschips.png" alt="Screenshot of the Microsoft edition of Chip's Challenge, showing the first level, courtesy of the BBC wiki">
</div>
<p>The most obvious explanation is that the developer responsible for the Microsoft port didn’t have access to the Lynx source code, and in fact, <em>had never played the original game at all</em>. That would explain nearly every major gameplay difference between the Lynx and Microsoft versions, which are all things you’d never notice if you only had static screenshots and maps to work from. Given that restriction, hey, not a bad job.</p>
<hr />
<p>I played the <span class="caps">BOWEP</span> edition of Chip’s Challenge as a kid and was completely enamoured. I suppose what got me the most was the same thing that I found so compelling about Doom: the ability to modify your environment, whether by using blocks to clear water or toggling green blocks or generating new monsters from a clone machine. Being able to affect my environment in (more or less) free-form ways felt curiously powerful.</p>
<p>…</p>
<p>Well, let’s not think about that too hard. I’ll save it for my therapist.</p>
<p>Some years later I discovered an incredible tool called <em>The Internet</em>, and with it I learned of the impending <a href="proxy.php?url=https://wiki.bitbusters.club/Chip%27s_Challenge_2">Chip’s Challenge 2</a>, a sequel with <em>way</em> more tiles and possibilities! Fantastic!</p>
<p>Unfortunately, there was a complication. Epyx, the original publisher of Chip’s Challenge, had gone bankrupt (somehow!) and had sold most of its assets, including the Chip’s Challenge rights, to a company called Bridgestone Media (now Alpha Omega Productions), a Christian propaganda distributor.</p>
<p>You read that correctly.</p>
<p>Bridgestone, a company that generally dealt in movies, had some very peculiar ideas about the video game industry. Apparently they expected the assets they’d acquired to magically make them filthy rich — you know, just like Jesus would want — despite having acquired them <em>from</em> a company that had just evaporated. As such, they told the original developer, Chuck Somerville, that he could only release Chip’s Challenge 2 if he paid them <a href="proxy.php?url=https://forum.bitbusters.club/thread-2127.html">one million dollars upfront</a>.</p>
<p>He did not have one million dollars, and so Chip’s Challenge 2 languished forever.</p>
<p>(At this point, in hindsight, I wonder why Chuck didn’t simply change the story and tileset and release the game under a different name. Apparently he did start on something like this some years later, in the form of an open clone <em>from scratch</em> called <a href="proxy.php?url=https://wiki.bitbusters.club/Puzzle_Studio">Puzzle Studio</a>, but it was eventually abandoned in favor of <a href="proxy.php?url=https://wiki.bitbusters.club/Chuck's_Challenge_3D">Chuck’s Challenge 3D</a>. But I still wonder: why start a brand new thing, rather than rebrand and release the existing thing?)</p>
<p>We did have some descriptions of new Chip’s Challenge 2 mechanics, and so at the ripe old age of 15, with no idea what I was doing, I decided I would simply write my own version of Chip’s Challenge 2.</p>
<p>In QBasic.</p>
<p>Also I didn’t really understand how to handle the passage of time, so the game was turn-based and had no monsters.</p>
<p>But, given all that, it wasn’t <em>that</em> bad. I found the source code a few years ago and <a href="proxy.php?url=https://gist.github.com/eevee/1b371c4b2470dd82cbcf">put it on GitHub</a> along with a sample level and a description of all the tiles you can use in the plaintext level format. I’ve got a <a href="proxy.php?url=https://c.eev.ee/CHIPS.EXE">prebuilt binary for <span class="caps">DOS</span></a> (usable in DosBox) too, if you like — just have a <code>levels.txt</code> in the same directory, and be sure it uses <span class="caps">DOS</span> line endings. I used to have one or two actual levels, but they have tragically been lost to the sands of time.</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/updates/lexys-labyrinth/qbchips.png" alt="Screenshot of my QBasic implementation of Chip's Challenge, using all character-based graphics">
</div>
<p>That would’ve been 2002.</p>
<hr />
<p>Thirteen years later, in April 2015, a miracle occurred and defeated the Christians. Chip’s Challenge 2 was released <a href="proxy.php?url=https://store.steampowered.com/app/348300/Chips_Challenge_2/">on Steam</a>.</p>
<p>It was fine. I don’t know. Over a decade of anticipation gets your hopes up, maybe. It’s a perfectly good puzzle game, and I don’t want to dunk on it, but sometimes I interact with it and I feel all life drain from my body.</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/updates/lexys-labyrinth/cc2female1.png" alt="Screenshot of CC2, with an overlaid hint saying: "This is Melinda. Being female, she does some things differently than Chip."">
<img src="proxy.php?url=https://eev.ee/media/updates/lexys-labyrinth/cc2female2.png" alt="Screenshot of CC2, with an overlaid hint saying: "She doesn't slide when she steps on ice. But she needs hiking boots to walk on dirt or gravel."">
</div>
<p>I don’t even know whether to talk about this completely unreadable way of showing hints or the utterly baffling justification of “being female” for these properties.</p>
<p>But it’s fine. The game was Windows-only, but it was <em>old</em> Windows-only, so Wine handled it perfectly well. I played through a few dozen levels. Passwords were gone, so you were free to skip over levels you just didn’t feel like playing.</p>
<p>And then they patched a level editor into the game, and it completely broke under Wine. <em>Completely</em>. Like, would not even run. It’s only in recent years that it even <em>tries</em> to run, and now it can’t draw the window and crashes if you attempt to do anything.</p>
<p>The funny thing is, apparently it doesn’t draw for some people on Windows, either. It doesn’t for <em>me</em> in a Windows <span class="caps">VM</span>. The official sanctioned solution is to… install… wined3d, a Windows port of the Wine implementation of Direct3D.</p>
<p>I don’t know. I don’t know! I don’t know what the hell anything. This situation is utterly baffling. What even are computers.</p>
<hr />
<p>I gave up on the game until recently, when something reminded me of it and I tried it again in Wine. No luck, obviously. I spent half a day squabbling with bleeding-edge versions and Proton patches and all manner of other crap, then resorted to the <a href="proxy.php?url=https://bitbusters.club/">Bit Busters Club</a> <a href="proxy.php?url=https://discord.gg/Xd4dUY9">Discord</a>, but they couldn’t help me either.</p>
<p>And then something stirred, deep inside of me. This game wasn’t <em>that</em> complicated, right? I actually know how to make video games now. I even know how to make art, sort of. And sound. And music. And…</p>
<hr />
<p>And here I am, a month later, having replicated Chip’s Challenge in a web browser, fueled entirely by some new emotion I’ve discovered that lies halfway between spite and exhaustion. My real goal was to clone Chip’s Challenge 2 <em>so I can actually fucking play this game I bought</em>, but it is of course a more complex game. Still, <span class="caps">CC2</span> support is something like 60% done; most of what remains is wiring, tracks, and ghost/rover behavior.</p>
<p><span class="caps">CC1</span> support is more interesting, anyway — there are far more custom <span class="caps">CC1</span> levels around, and Lexy’s Labyrinth exposes almost 600 of them a mere click away. Given that the original Microsoft port was 16-bit and is now difficult to run (and impossible to buy), and the official (free!) <a href="proxy.php?url=https://store.steampowered.com/app/346850/Chips_Challenge_1/">Steam release</a> is fairly awkward and unmaintained (the dev mostly makes vague statements about “old code”), and even the favored emulator <a href="proxy.php?url=https://wiki.bitbusters.club/Tile_World">Tile World</a> has the aesthetics and usability of a 1991 Unix application, I’m hoping this will make the Chip’s Challenge experience a little more accessible. It has a partially working level editor, too, which lets you share levels you make by simply passing around a <span class="caps">URL</span>, and I think that is fucking fantastic.</p>
<p><span class="caps">LL</span> cannot currently load level <em>packs</em> from the Steam release, but it’s a high priority. In the meantime, if you really want to play the original levels (even though <span class="caps">CCLP1</span> is far better in my experience), it’ll load <code>CHIPS.DAT</code> if you’ve got it lying around. Also, it works on phones!</p>
<hr />
<p>Probably the most time-consuming parts of this project were the assets. I had to draw a whole tileset from scratch, <em>including</em> all of the <span class="caps">CC2</span> tiles which you don’t even get to see yet (and a few of which aren’t actually done). That probably took a week, spread out over the course of the entire last month. Sound effects took several days, though they got much easier once I decided to give up on doing them by wiring LFOs together in SunVox and just use a bunch of <a href="proxy.php?url=https://www.beepbox.co/">BeepBox</a> presets. I spent a couple days on my own music track, and half a dozen other kind souls chipped in their own music — thank you so much, everyone!</p>
<p>And thank you to the Bit Busters Club, whose incredibly detailed knowledge made it possible to match the behavior of a lot of obscure-but-important interactions. The Steam version of <span class="caps">CC1</span> comes with solution replays, and <span class="caps">LL</span> can even play a significant number of them back without ever desyncing.</p>
<p>I’ve been ignoring pretty much everything else for a month to get this in a usable state, so I’d like to take a break from it for now, but I’d really like to get all of <span class="caps">CC2</span> working when I can, and of course make the level editor fully functional. I love accessible modding tools, you don’t see many of them in games any more, and with any luck maybe it’ll inspire some other kid to get into game development later.</p>
<hr />
<p>…okay, I haven’t been ignoring <em>everything</em> else. I also reused the tiles I drew for a fox flux minigame in a similar style, except that you place a limited set of tiles in empty spaces and then let the game run <em>by itself</em>. Kind of like… Chip’s Challenge meets The Incredible Machine.</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/updates/lexys-labyrinth/fox-flux-minigame-demo.gif" alt="Recording of a minigame, showing a drone character interacting with moving floors and following instructions on the ground">
</div>
<p>(That arrow tile has since been updated to be more clear, but it means “when you hit something, turn around instead of stopping and ending the game.”)</p>
<p>I guess two little puzzle game engines isn’t too bad for not quite a month of work!</p>fox flux, three years later2020-08-04T13:50:00-07:002020-08-04T13:50:00-07:00Eeveetag:eev.ee,2020-08-04:/blog/2020/08/04/fox-flux-three-years-later/<p>I’m working on a video game! Like, a serious one.</p>
<p>I’m working on a video game! Like, a serious one.</p>
<h2 id="the-past"><a class="toclink" href="proxy.php?url=#the-past">The past</a></h2>
<p>I wrote <a href="proxy.php?url=https://eevee.itch.io/fox-flux">the original game</a> (very slightly <span class="caps">NSFW</span>) for my own “horny” game jam, <a href="proxy.php?url=https://itch.io/jam/strawberry-jam">Strawberry Jam</a> (more likely to be <span class="caps">NSFW</span>), way back in February 2017.</p>
<p>You play as Lexy, my shameless Floraverse self-insert, who owns an enchanted collar that (among other things) makes her basically indestructible and allows her to easy to transform into… whatever, given some kind of sensible trigger. And then you do some puzzle-platforming to collect “strawberry hearts” and gain access to new areas, much of which (surprise!) involves getting turned into things.</p>
<p>For example, this chain-link fence blocks you:</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/dev/media/fox-flux/jam-example-fence.png" alt="Screenshot of the player being stuck on one side of a fence">
</div>
<p>But if you let that green blob in the grass turn you into slime, you can walk right through it.</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/dev/media/fox-flux/jam-example-fence-slime.png" alt="Screenshot of the same area, but the player is now green slime and free to pass through the fence">
</div>
<p>There are also spikes, which you get stuck on if you land on them… but slime can walk right through them, glass can stand on top of them, and stone outright destroys them. And so on. As a jam game, it’s not very expansive, but many of the puzzle elements interact differently with many of the handful of Lexy variants, which provided enough potential to make eight levels.</p>
<h3 id="post-jam"><a class="toclink" href="proxy.php?url=#post-jam">Post-jam</a></h3>
<p>The jam game was rough, but I really liked the concept and wanted to expand on it. I spent a good chunk of the summer of 2017 on it, but it was a struggle. I was still fairly new to pretty much every aspect of actually creating a game — I’d only been drawing for two years, I’d sometimes hit big gaps in the design with no idea how to fill them, and I wasn’t yet entirely comfortable with complex physics or shaders. The art in particular was a huge problem; it took me a long time to produce sprites that I was only passably happy with. My spouse Ash is an artist, and we’ve made several games together where they produced all the art, but this was <em>my</em> idea and I was determined to draw it myself.</p>
<p>Then 2018 hit, which was a whole entire mess, and I didn’t really touch fox flux at all for over a year. I made a couple of other games with Ash, some finished, some not, and kept drawing intermittently.</p>
<p>I returned to fox flux for the middle of 2019, and decided… I’m not sure what I decided, exactly. I guess I’d gotten better at all the things that had been difficult for me before, so I set about trying to improve every aspect of the game at once.</p>
<ul>
<li>
<p>I realized the (many, <em>many</em>) improved sprites I’d drawn in 2017 were not actually very good, and drew a new Lexy design from scratch that absolutely blew me away… which meant throwing away all the existing art.</p>
</li>
<li>
<p>I’d come up with a few new things for Lexy to turn into, each of which altered her behavior pretty significantly, and her code was becoming a spaghetti disaster. So I spent some time completely refactoring actors into bags of components, which I was unsure about until very recently and which ended up breaking pretty much every single object in the game, sometimes in subtle ways.</p>
</li>
<li>
<p>I decided to add water, which unraveled into a whole pile of decisions and problems.</p>
</li>
<li>
<p>I tried to make consistent or interesting physics for pushing things (e.g. wooden crates), and that became a <em>nightmare</em>. I easily spent weeks on this, trapped in a cycle of finding some edge case that couldn’t be fixed without considerably expanding what I was simulating, struggling to do that expansion while keeping all the basic stuff working, and then finding a new and different edge case.</p>
</li>
</ul>
<p>Did I mention that I tried to do all of these things at the same time, while <em>also</em> trying to nail down the design of a game that’s naturally prone to a combinatoric explosion of interactions?</p>
<p>At a certain point it just felt hopeless. I’d poured easily over a year into this game, and all I had to show for it was a jumbled pile of stuff that didn’t work, strewn about a couple test maps that didn’t even contain any puzzles.</p>
<h2 id="the-present"><a class="toclink" href="proxy.php?url=#the-present">The present</a></h2>
<p>I don’t know what happened, exactly. I’d given up on the heavily-simulated push physics last year, at least, so that wasn’t so much of a concern any more. But I still had a mess. I’d long since written <code>git status</code> off as unusable.</p>
<p>Until this past month, when I sat down and just started powering through the mess. One by one, I fixed the serious breakages that the component refactor had caused. I dedicated a day or two just to figuring out water physics, put a little more thought into it, and ended up with something that looks and plays quite nicely. I finished redrawing basic Lexy, and even added frames I hadn’t had before.</p>
<p>I think the difference was… fear. I’d previously hesitated <em>so much</em>, both in the art and the gnarlier code. It was such a struggle to get something working <em>at all</em> that changing it in any way was terrifying — what if I broke it and couldn’t even get it back to how it’d been?</p>
<p>I don’t know how to describe exactly how this felt, and I also don’t know how to explain what changed. It was like a switch flipped. I think it started when I drew new dirt tiles, and it didn’t even take that long, and I <em>loved</em> them. I’ve always had a hard time drawing terrain, and for once I just sat down and did it and it came out well and it looked like <em>mine</em>, like my style, which was a thing I hadn’t even really grasped I have before. After that I just cranked out a mountain of new sprite art, faster and better than anything I’d done before. Like I’d been accumulating <span class="caps">XP</span> over the past few years and just now decided to spend it all on levelling up.</p>
<p>Over the past six weeks, I have:</p>
<ul>
<li>Redesigned the terrain</li>
<li>Vastly improved the palette</li>
<li>Completely finished redrawing Lexy</li>
<li>Redesigned the <span class="caps">HUD</span></li>
<li>Mocked up a new dialogue layout</li>
<li>Drawn a new font</li>
<li>Drawn and implemented new consistent level entrances</li>
<li>Animated a treasure chest opening cutscene</li>
<li>Animated getting a key</li>
<li>Added a completely new tally at the end of a level</li>
<li>Added transitions for entering and leaving levels</li>
<li>Added swimming behavior</li>
<li>Redrawn the old gecko as a much more visible bananalizard</li>
<li>Animated the hearts and several other pickups</li>
<li>Ported the original forest levels to use all the new stuff</li>
<li>I don’t even know there has been just so much</li>
</ul>
<p>Just look at the style evolution! God damn.</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/dev/media/fox-flux/style-evolution.png" alt="Three versions of Lexy in dirt tiles; over time, the style becomes more colorful and relies on stronger shapes and silhouettes">
</div>
<p>Here’s that same level from above:</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/dev/media/fox-flux/deluxe-example-fence.png" alt="Slime Lexy once again passing freely through the fence, but using newer assets">
</div>
<p>A lot of the last few weeks went towards level transitions, which previously… kind of worked. They were always a hasty jam hack that I never liked; there was a quick screen fade when going through a door, there was barely any notion of being “in a level” vs not, and the game even counted the fucking hearts in a level on the fly the first time you entered it. It was all very silly.</p>
<p><em>But now</em> (please pardon the occasional frame drops from my screen recorder):</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/dev/media/fox-flux/level-loop.gif" alt="GIF of Lexy entering a level with a transition, collecting candy, exiting with another transition, and seeing the level tally">
</div>
<p>I finally feel like I’m making some real progress. I finally feel like this could be something I take seriously, that it could be a <em>real game</em>, something more than half an hour long. At some point it just became an absolute joy to look at and run around in.</p>
<h3 id="the-idea"><a class="toclink" href="proxy.php?url=#the-idea">The idea</a></h3>
<p>The basic concept is the same, but I want to add some structure to it. The jam game was four single-room levels you could tackle in any order without much guidance, then another set of the same. Which is fine, but doesn’t give me much wiggle room in the design.</p>
<p>In the full game, levels will contain not just hearts, but also a treasure (a la Wario Land 3), some amount of candy (usable at the <em>shop</em> to buy <a href="proxy.php?url=https://twitter.com/foxfluxDELUXE/status/1289626941637550083"><em>things</em> of <em>some description</em></a>), and an explicit exit. The overworld will function a bit more like a world map, and though you’ll still need to collect N hearts to get to the next zone, there may sometimes be obstacles that can only be overcome by finding the right treasure in a level.</p>
<p>I also intend to give Lexy some active abilities, for example this blown kiss (recorded with older art) that can toggle pink objects between two states:</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/dev/media/fox-flux/kiss.gif" alt="Lexy blows a kiss towards a pink brick wall, which changes it into a pink grating">
</div>
<p>I even have a plot in mind! The jam game had only a teeny tiny one.</p>
<h2 id="the-future"><a class="toclink" href="proxy.php?url=#the-future">The future</a></h2>
<p>Ash is currently busy with their own game, so I think this is gonna be The Thing I Do for a while. To that end, I’m in the middle of setting up some infrastructure:</p>
<ul>
<li>A dedicated <a href="proxy.php?url=https://twitter.com/foxfluxDELUXE">Twitter account</a></li>
<li>An <a href="proxy.php?url=https://eevee.itch.io/fox-flux-deluxe">itch.io page</a></li>
<li>A <a href="proxy.php?url=https://discord.gg/8aBpBQW">Discord channel</a></li>
</ul>
<p>Also, I recently created a <em>secret</em> Discord channel on the same server, where I intend to do planning and design work that I’m not ready to make public yet! Spoilers will abound, but if you’re interested and okay with that, you can get in by pledging at least $4 on <a href="proxy.php?url=https://www.patreon.com/eevee">Patreon</a> and letting me know to give you the role. (I don’t use Patreon’s native Discord integration because it does rude things like forcibly rejoin you to the server even if you manually leave.)</p>
<h3 id="specific-priorities"><a class="toclink" href="proxy.php?url=#specific-priorities">Specific priorities</a></h3>
<p>I’d like to finish porting the old levels over to new artwork, the new level infrastructure, etc. It’d make for a nice little Patreon demo or something, it gives me a milestone with pretty clear goals, and it’ll leave me with at least a small palette of puzzle elements that I <em>know</em> work correctly.</p>
<p>I’d like to write about what I’m doing sometimes on this dang blog. I’ve found that structured writing is really, really, <em>really</em> hard when my head is a mess, and it has been extremely a mess for the last two and a half years (sorry), but jotting down what I’m already doing should be much easier than the more elaborate posts I’ve written, which need research and tooling and whatnot.</p>
<p>I have a good handful of puzzle elements — some of which even work — and a bunch of ideas for more, but I haven’t actually tried <em>building levels</em> since I made the original game! That’s kind of the important part, so I’d love to do some of it now that the dust is finally settling.</p>
<p>I still have some design decisions to make, though they’re getting trickier since I’ve already decided all the easy stuff. But I’ll save that for the generous folks who give me four dollars, I guess.</p>
<h3 id="the-elephant-in-the-room"><a class="toclink" href="proxy.php?url=#the-elephant-in-the-room">The elephant in the room</a></h3>
<p>So. As I mentioned at the beginning, this game was originally made for a “horny” game jam. Given that it’s mostly platforming, you might be wondering why that is. I already feel like I’m crossing the streams somehow by even mentioning this on this blog, so I’ll try very hard not to get <span class="caps">TMI</span> here.</p>
<p>I have a foot in “<span class="caps">TF</span>” (transformation) kink circles, and one thing that’s always struck me about that subculture is how much of it is completely non-sexual. You can find no end of artwork of, say, someone being turned into one of those inflatable pooltoys — where both the artist and the audience are obviously having a good time with it — yet with no hint of sexual elements whatsoever. It’s a form of sexuality that doesn’t need to be sexual at all.</p>
<p>I started Strawberry Jam because I wanted to see some adult games that were more creative with their gameplay. Much of the genre consists of otherwise regular games that occasionally show you some explicit artwork, and while that’s a perfectly fine way to design a game, I felt that the medium surely had more potential. It turns out that a non-sexual fantasy kink works wonders as a gameplay element; rather than just giving you a picture, the game takes a concept and has you <em>experience it yourself</em>, even figure out by experimentation how it’s altered the way you interact with the world.</p>
<p>This puts me in a slightly awkward position. I do, genuinely <em>and platonically</em>, love these kinds of gameplay themes! I adore changes in how you perceive or interact with a world — the dark world in Metroid Prime 2, the time reversal in Braid, the “dimension” swapping in Quantum Conundrum, etc. I think this is a great concept that anyone can have a good time with, and I feel like this game is a love letter to the Wario Land series.</p>
<p>At the same time, I do <em>also</em> appreciate the kink inspiration. Even Lexy’s collar was originally conceived as a gimmick I could use for drawing adult artwork. The jam game contains a lot of suggestive dialogue, since Lexy herself also appreciates the kink aspect. And that was a lot of fun to write, and I’m sure it enhanced the experience for other folks with similar leanings.</p>
<p>But this is such a good concept that I want it to be playable as <em>just</em> a regular puzzle-platformer as well. I think it would have fairly broad appeal, and I don’t want to hamstring myself by totally fucking weirding people out when it dawns on them that “oh the dev is kinda Into This huh”. And yet I don’t want to completely sterilize the game, either, because… well, ultimately, it’s my game and I <em>like</em> the suggestive parts.</p>
<p>This is a tough line to draw, and I’m not yet sure how to do it. I’ve considered just making alternative dialogue that you can opt into when you start the game, but given that Lexy already speaks differently depending on what form she’s in, I have no idea how feasible that is.</p>
<p>I don’t know how to gauge this. I’ve always been up to my armpits in the side of the internet that just posts porn and talks about sexuality casually, whereas I’m dimly aware that most people see sexuality as this completely distinct part of life that you hide in a small box, far away from the eyes of polite society. But maybe I’m overestimating that? Does anyone actually care if the protagonist of a game comments “hey this is hot” about something weird but innocuous?</p>
<p>Or maybe that’s exactly where the line is. I remember Nier: Automata, a game that is all too happy to show off the protagonist’s immaculately-rendered ass, which is clearly meant for the enjoyment of both the creator and the players. But nobody comments on it <em>within the game</em>, which makes it seem incidental, somehow. I can’t explain why that is, and it feels slightly dishonest to me.</p>
<p>Am I overthinking this? If you’re <em>not</em> involved in any kind of kink circles and played the original jam game, I’m curious to hear how it read to you. Was it at all uncomfortable, like perhaps the game was expecting you to heavily empathize with a feeling you don’t share at all? Or does putting that feeling on a <em>character</em>, rather than aiming it at the human player, make it something you can easily shrug off? The full game will have more stuff going on, so there should be lots more dialogue that <em>isn’t</em> solely about Lexy’s feelings, if that helps.</p>
<hr />
<p>Hm, I thought I would have more to say here! I have a lot of ideas, but only a handful of them are implemented yet, and I guess it’s hard to show what a game will be like before most of it works.</p>
<p>I hope this is enough to whet some appetites, at least! I haven’t been excited like this about <em>anything</em> in far too long.</p>Rowling is dangerously wrong2020-06-11T11:15:00-07:002020-06-11T11:15:00-07:00Eeveetag:eev.ee,2020-06-11:/blog/2020/06/11/rowling-is-dangerously-wrong/<p>I read <span class="caps">J.K.</span> Rowling’s <a href="proxy.php?url=https://www.jkrowling.com/opinions/j-k-rowling-writes-about-her-reasons-for-speaking-out-on-sex-and-gender-issues/">essay</a>.</p>
<p>I regret doing so.</p>
<p>Here are some thoughts. Trans readers, brace yourselves, especially if you didn’t read the original.</p>
<p>Some help came from <a href="proxy.php?url=https://twitter.com/Carter_AndrewJ/status/1270787941275762689">Andrew James Carter’s response thread</a>, which has many more citations but feels less compelling to a general audience to me.</p>
<p>I read <span class="caps">J.K.</span> Rowling’s <a href="proxy.php?url=https://www.jkrowling.com/opinions/j-k-rowling-writes-about-her-reasons-for-speaking-out-on-sex-and-gender-issues/">essay</a>.</p>
<p>I regret doing so.</p>
<p>Here are some thoughts. Trans readers, brace yourselves, especially if you didn’t read the original.</p>
<p>Some help came from <a href="proxy.php?url=https://twitter.com/Carter_AndrewJ/status/1270787941275762689">Andrew James Carter’s response thread</a>, which has many more citations but feels less compelling to a general audience to me.</p>
<hr />
<blockquote>
<p>This isn’t an easy piece to write, for reasons that will shortly become clear, but I know it’s time to explain myself on an issue surrounded by toxicity. I write this without any desire to add to that toxicity.</p>
</blockquote>
<p>I admire that. I, too, would prefer not to add to the toxicity.</p>
<blockquote>
<p>For people who don’t know: last December I tweeted my support for Maya Forstater, a tax specialist who’d lost her job for what were deemed ‘transphobic’ tweets. She took her case to an employment tribunal, asking the judge to rule on whether a philosophical belief that sex is determined by biology is protected in law. Judge Tayler ruled that it wasn’t.</p>
</blockquote>
<p>We are off to a poor start. Framing an unrenewed contract as “losing her job” is dubious. And specifically, Judge Tayler <a href="proxy.php?url=https://drive.google.com/file/d/12P9zf82TicPs2cCxlTnm0TrNFDD8Gaz5/view">ruled</a> that “she will refer to a person by the sex she considered appropriate even if it violates their dignity and/or creates an intimidating, hostile, degrading, humiliating or offensive environment” — that is, she would be actively and knowingly rude towards people in the workplace, and <em>that</em> is not protected.</p>
<p>(Forstater later disingenuously claimed to have lost her job for “speaking up about women’s rights”. And I’m just now learning that she compared the use of correct pronouns to the use of rohypnol — the date rape drug — <em>while</em> this court case was pending. <a href="proxy.php?url=https://www.nbcnews.com/think/opinion/j-k-rowling-s-maya-forstater-tweets-support-hostile-work-ncna1105201">Charming.</a>)</p>
<blockquote>
<p>All the time I’ve been researching and learning, accusations and threats from trans activists have been bubbling in my Twitter timeline. This was initially triggered by a ‘like’. When I started taking an interest in gender identity and transgender matters, I began screenshotting comments that interested me, as a way of reminding myself what I might want to research later. On one occasion, I absent-mindedly ‘liked’ instead of screenshotting. That single ‘like’ was deemed evidence of wrongthink, and a persistent low level of harassment began.</p>
</blockquote>
<p>This sounds like a simple misunderstanding which could have been resolved with a single explanatory tweet. Instead, your spokesperson <a href="proxy.php?url=https://www.pinknews.co.uk/2018/03/22/jk-rowling-reps-blame-middle-aged-moment-for-liking-tweet-calling-trans-women-men-in-dresses/">referred</a> to it as a “clumsy and middle-aged moment”. And now you categorize the tweet vaguely as something to research — suggesting to a casual reader that you had merely liked a link to a scholarly article, perhaps — when it was a mundane personal rant which referred to trans women as “men in dresses”.</p>
<p>I have a hypothesis about where the toxicity began — right there, when you clicked the heart underneath it. It’s something you know is mean and hurtful to the people it describes, and is <em>intended</em> to be so, and you not only defend it but cloak it in an obligatory 1984 reference. This is deceptive, mean-spirited, and shameful.</p>
<p>We are only on paragraph four.</p>
<blockquote>
<p>Months later, I compounded my accidental ‘like’ crime by following Magdalen Burns on Twitter. Magdalen was an immensely brave young feminist and lesbian who was dying of an aggressive brain tumour. I followed her because I wanted to contact her directly, which I succeeded in doing. However, as Magdalen was a great believer in the importance of biological sex, and didn’t believe lesbians should be called bigots for not dating trans women with penises, dots were joined in the heads of twitter trans activists, and the level of social media abuse increased.</p>
</blockquote>
<p><span class="dquo">“</span>You are fucking blackface actors. You aren’t women. You’re men who get sexual kicks from being treated like women. fuck you and your dirty fucking perversions. our oppression isn’t a fetish you pathetic, sick, fuck.”</p>
<p>That’s what Magdalen Berns, whose name you misspelled, had to say about trans women. (Ironically, it’s not too far off from what folks used to say — and occasionally still do — about gay folks.) I’m going to hazard a guess that this was more of a concern than any discourse about who lesbians choose to date.</p>
<p>The funny thing is, while I’ve seen the “gender critical” crowd complain <em>numerous</em> times that trans women are somehow trying to force cis lesbians to have sex with them (by tweeting about it?), I’ve virtually never witnessed the phenomenon directly — and I am <em>neck-deep</em> in trans Twitter. Perhaps two or three times over the years, I’ve seen some discourse about “genital attraction” and whether it’s socially influenced, which I suppose is an interesting question. On one singular occasion, such a tweet came uncomfortably close to suggesting that people were obligated to correct for what’s presumed to be social influence in who they’re attracted to, and I swiftly pushed back against it.</p>
<p>But the way “gender critical” folks talk about this, you’d think it was the only topic trans women ever discuss! Meanwhile, do you know who most trans women I know are dating? <em>Each other!</em></p>
<blockquote>
<p>I mention all this only to explain that I knew perfectly well what was going to happen when I supported Maya. I must have been on my fourth or fifth cancellation by then.</p>
<p>I expected the threats of violence, to be told I was literally killing trans people with my hate, to be called cunt and bitch and, of course, for my books to be burned, although one particularly abusive man told me he’d composted them.</p>
</blockquote>
<p>I am genuinely sorry that people are abusive on Twitter, but I don’t know how to avoid it when you have more followers than the populations of <span class="caps">NYC</span> and <span class="caps">LA</span> combined. It’s a much broader problem, though definitely exacerbated when you support someone who has been fighting for the right to be deliberately hostile.</p>
<p>I’m not sure what to make of the last part. Is composting a book worse than burning it? And are you hinting a comparison between burning one’s own personal property and the actions of Nazi Germany, or am I reading too much into this conspicuous phrasing? I hope the latter, because the former would be <em>extremely</em> tasteless, considering that part of what was burned was the research and library of <a href="proxy.php?url=https://en.wikipedia.org/wiki/Institut_f%C3%BCr_Sexualwissenschaft">a sex research institute</a> which was <em>founded by</em> the man who coined the term “transsexualism” and had trans people as both staff and clients.</p>
<blockquote>
<p>What I didn’t expect in the aftermath of my cancellation was the avalanche of emails and letters that came showering down upon me, the overwhelming majority of which were positive, grateful and supportive. They came from a cross-section of kind, empathetic and intelligent people, some of them working in fields dealing with gender dysphoria and trans people, who’re all deeply concerned about the way a socio-political concept is influencing politics, medical practice and safeguarding. They’re worried about the dangers to young people, gay people and about the erosion of women’s and girl’s [sic] rights. Above all, they’re worried about a climate of fear that serves nobody – least of all trans youth – well.</p>
</blockquote>
<p>I note, conspicuously, that zero of them were <em>from trans people</em>, or you surely would’ve mentioned as much. You give trans youth a token mention at the end, but only as an object of external concern, not as people to be listened to and trusted about their own experiences. This is a theme that I see we’ll be revisiting.</p>
<blockquote>
<p>I’d stepped back from Twitter for many months both before and after tweeting support for Maya, because I knew it was doing nothing good for my mental health. I only returned because I wanted to share a free children’s book during the pandemic. Immediately, activists who clearly believe themselves to be good, kind and progressive people swarmed back into my timeline, assuming a right to police my speech, accuse me of hatred, call me misogynistic slurs and, above all – as every woman involved in this debate will know – <span class="caps">TERF</span>.</p>
</blockquote>
<p>I note for the audience that the “gender critical” crowd — you know, TERFs — love to use the term <span class="caps">TRA</span> (trans rights activist) to refer to pretty much any trans person who doesn’t buy what they’re selling. I don’t know if this is meant to be a dogwhistle, but it at least quacks like one.</p>
<p>More generally, “activists” is a favored scare word across the political spectrum, much like “ideology” — it conjures the image of someone who is angrily trying to push Politics on you, while neatly obscuring that the political view they’re trying to push is “please don’t be cruel to me or people like me”. Are you, Rowling, not an activist? What about the people you support, like Berns? You use “activist” ten times in this essay, and every single time to describe trans people.</p>
<p>It’s rhetorical sleight of hand. Trans people who want to live their lives without being called blackface actors are “activists”, while the people making those comments are merely expressing concerns. Telling people what they should be able to <em>wear</em> earns no mention in this essay at all, but replying on a public platform to tell you that you are being hurtful is “policing your speech”.</p>
<p>Do you know where I first learned about this trick? From people who opposed the gay rights movement. “Gay rights activist” was a phrase I saw bandied about a <em>lot</em> while I was growing up, as though wanting to be able to marry one’s partner instantly transformed a person into some sort of unreasonable lobbyist, while opposing it was just the normal and natural thing to do. Frequently they’d have one gay person who agreed with them to put on a pedestal, the proof that they didn’t actually hate gay people — at least not the ones who’d sit down and shut up and accept whatever scraps they were given.</p>
<blockquote>
<p>If you didn’t already know – and why should you? – ‘<span class="caps">TERF</span>’ is an acronym coined by trans activists, which stands for Trans-Exclusionary Radical Feminist. In practice, a huge and diverse cross-section of women are currently being called TERFs and the vast majority have never been radical feminists. Examples of so-called TERFs range from the mother of a gay child who was afraid their child wanted to transition to escape homophobic bullying, to a hitherto totally unfeminist older lady who’s vowed never to visit Marks <span class="amp">&</span> Spencer again because they’re allowing any man who says they identify as a woman into the women’s changing rooms.</p>
</blockquote>
<p>As any best-selling author would know, if a word is used incorrectly at least two times on Twitter, it loses all meaning.</p>
<p>From what I’ve observed, the vast majority of people referred to as TERFs are people who claim an interest in the well-being of women and lesbians, but exclude trans women from that (or outright classify them all as predators), treat trans men as confused women, speak over or outright ignore the people they claim to be defending, and spend an awful lot of time inventing or vastly exacerbating “concerns” about trans people so as to excuse spending an awful lot of the rest of their time saying incredibly nasty things.</p>
<blockquote>
<p>Ironically, radical feminists aren’t even trans-exclusionary – they include trans men in their feminism, because they were born women.</p>
</blockquote>
<p>This <em>is</em> trans-exclusionary. It’s feminism that ignores and talks over trans men, which is a strange thing for feminists to do to people they consider to be women.</p>
<blockquote>
<p>But accusations of TERFery have been sufficient to intimidate many people, institutions and organisations I once admired, who’re cowering before the tactics of the playground. ‘They’ll call us transphobic!’ ‘They’ll say I hate trans people!’ What next, they’ll say you’ve got fleas?</p>
</blockquote>
<p>Not wanting to come across as hating a group of people is generally considered polite. Imagine saying this about, I don’t know, lesbians.</p>
<blockquote>
<p>Speaking as a biological woman, a lot of people in positions of power really need to grow a pair (which is doubtless literally possible, according to the kind of people who argue that clownfish prove humans aren’t a dimorphic species).</p>
</blockquote>
<p>Is “courage is stored in the balls” feminist now?</p>
<p>But since you bring up dimorphism, here’s a fun anecdote that’s relevant to my field. It seems that one of the biggest factors a neural network (“<span class="caps">AI</span>”) uses to determine a person’s gender is… <a href="proxy.php?url=https://medium.com/@kerryrodden/is-that-a-boy-or-a-girl-cb93abbae6da">hair length</a>! Which isn’t a dimorphic trait, at least not how you’d think. The sexes are not really all that distinct; much of it is decoration we put on ourselves to exacerbate the differences, for <a href="proxy.php?url=https://twitter.com/dorrismccomics/status/1270032808434696193">some reason</a>.</p>
<p>For some more anecdotes, feel free to look for reports of cis lesbians being kicked out of public women’s restrooms for looking too masculine. Like <a href="proxy.php?url=https://www.foxnews.com/wires/2008May13/0,4670,GayCustomerLawsuit,00.html">this one</a>, or <a href="proxy.php?url=https://www.gaystarnews.com/article/lesbian-kicked-out-of-bowling-alley-because-she-used-the-womens-restroom/">this one</a>, or <a href="proxy.php?url=https://www.towleroad.com/2016/04/police-force-lesbian-to-leave-bathroom-for-failing-to-show-id-prove-shes-a-woman-watch/">this one</a>, or <a href="proxy.php?url=https://www.mirror.co.uk/news/uk-news/lesbian-couple-kicked-out-womens-4977298">this one</a>. Whose activism do you suppose would exacerbate this?</p>
<blockquote>
<p>Firstly, I have a charitable trust that focuses on alleviating social deprivation in Scotland, with a particular emphasis on women and children. Among other things, my trust supports projects for female prisoners and for survivors of domestic and sexual abuse. I also fund medical research into <span class="caps">MS</span>, a disease that behaves very differently in men and women. It’s been clear to me for a while that the new trans activism is having (or is likely to have, if all its demands are met) a significant impact on many of the causes I support, because it’s pushing to erode the legal definition of sex and replace it with gender.</p>
</blockquote>
<p>What a perfect example. What does it <em>mean</em> for <span class="caps">MS</span> to behave very differently in men and women? “Man” versus “woman” isn’t a switch you flip; it’s a combination of dozens of factors. If the difference is caused by hormone levels — which <a href="proxy.php?url=https://www.hopkinsmedicine.org/health/conditions-and-diseases/multiple-sclerosis-ms/multiple-sclerosis-why-are-women-more-at-risk">looks plausible</a> — then trans women on <span class="caps">HRT</span> will be affected similarly to cis women, because they have the same levels of estrogen! And by excluding them — by insisting we talk only about “biological” “men” and “women” rather than specific biological factors — you are <em>miscategorizing</em> them for no reason.</p>
<blockquote>
<p>The second reason is that I’m an ex-teacher and the founder of a children’s charity, which gives me an interest in both education and safeguarding. Like many others, I have deep concerns about the effect the trans rights movement is having on both.</p>
</blockquote>
<p>Ah, you mean Lumos, the charity you cofounded with Baroness Emma Nicholson, who <em>just yesterday</em> <a href="proxy.php?url=https://www.pinknews.co.uk/2020/06/10/baroness-emma-nicholson-same-sex-marriage-equality-tweets-twitter-homophobia/">said that gay marriage is degrading women’s rights</a> after attempting to repeal it in 2013? <em>I</em> have some deep concerns about the effect this person will have on the well-being of gay teens — and she’s not a mere “activist” or “movement”, but a lawmaker! Strange company you keep. And that’s not even getting into how <a href="proxy.php?url=https://twitter.com/KatyMontgomerie/status/1267911095332876289">she called it pedophilia</a> for a trans charity’s website to have an escape button on it in case of abusive parents, a mere week and a half ago.</p>
<blockquote>
<p>The third is that, as a much-banned author, I’m interested in freedom of speech and have publicly defended it, even unto Donald Trump.</p>
</blockquote>
<p><span class="dquo">“</span>Much-banned”? You wrote one of the best-selling books of all time and <em>the</em> best-selling series of all time. You have sold at least one book for every fourteen humans alive and made almost a dozen movie deals. When you tweet, it trends for days and makes national headlines. Your freedom of speech is not at risk here — and if it were, you could probably afford to inscribe whatever you wanted to say on the face of the moon.</p>
<blockquote>
<p>The fourth is where things start to get truly personal. I’m concerned about the huge explosion in young women [sic] wishing to transition and also about the increasing numbers who seem to be detransitioning (returning to their original sex), because they regret taking steps that have, in some cases, altered their bodies irrevocably, and taken away their fertility. Some say they decided to transition after realising they were same-sex attracted, and that transitioning was partly driven by homophobia, either in society or in their families.</p>
</blockquote>
<p>Yes, it’s truly tragic that homophobia is still rampant, such as in the baroness you cofounded a charity with. Especially in parents. Incidentally, the most common reason given for detransitioning — which is <a href="proxy.php?url=https://transequality.org/sites/default/files/docs/usts/USTS-Full-Report-Dec17.pdf">pressure from a parent</a> (36%, see page 108); the next is harassment/discrimination (31%), followed by having trouble getting a job (29%). Most of the other reasons given were pressure from some other external source. Only 0.4% of the people in that survey reported detransitioning because they simply did not like transition. And, by the way, detransition (even temporarily) is several times more common in trans women than trans men.</p>
<p>If you really want to fight detransition, the most effective action you could take would be to delete this post. But you’re approaching this from the perspective that trans men are confused, just like swaths of homophobic parents have said of their gay children.</p>
<blockquote>
<p>Most people probably aren’t aware – I certainly wasn’t, until I started researching this issue properly – that ten years ago, the majority of people wanting to transition to the opposite sex were male. That ratio has now reversed. The <span class="caps">UK</span> has experienced a 4400% increase in girls [sic] being referred for transitioning treatment. Autistic girls [sic] are hugely overrepresented in their numbers.</p>
</blockquote>
<p>Of course they are. Trans people are <a href="proxy.php?url=https://www.eurekalert.org/pub_releases/2019-07/aru-sft071619.php">disproportionately autistic</a>, so this is to be expected. I’d think this would be cause for celebration — people are being treated who previously wouldn’t have been! That’s excellent progress.</p>
<p>But instead of celebrating it, you suggest here that autistic trans <strong>boys</strong> are being taken advantage of. No, worse; you suggest that autistic trans boys are incapable of making decisions about their own lives, and don’t even respect them enough to refer to them as they wish to be referred to. You speak over them, dismiss them as obviously wrong out of hand, and ignore how they wish to be referred to while pretending to care about their well-being. This is deeply condescending and appalling.</p>
<p>As an aside, it’s quite frustrating that you so frequently refuse to connect the dots — instead you leave a trail of breadcrumbs and let some haunting conclusion form in the reader’s head, while retaining plausible deniability for yourself because you never actually <em>said</em> the things you’re trying to imply. That leaves you free to claim that a response like this one, which spells out the winks and nods, is yet more dismissable harassment.</p>
<blockquote>
<p>The same phenomenon has been seen in the <span class="caps">US</span>. In 2018, American physician and researcher Lisa Littman set out to explore it. In an interview, she said:</p>
<p>‘Parents online were describing a very unusual pattern of transgender-identification where multiple friends and even entire friend groups became transgender-identified at the same time. I would have been remiss had I not considered social contagion and peer influences as potential factors.’</p>
<p>Littman mentioned Tumblr, Reddit, Instagram and YouTube as contributing factors to Rapid Onset Gender Dysphoria, where she believes that in the realm of transgender identification ‘youth have created particularly insular echo chambers.’</p>
<p>Her paper caused a furore. She was accused of bias and of spreading misinformation about transgender people, subjected to a tsunami of abuse and a concerted campaign to discredit both her and her work. The journal took the paper offline and re-reviewed it before republishing it.</p>
</blockquote>
<p>This is probably because “rapid-onset gender dysphoria” is <a href="proxy.php?url=https://www.buzzfeednews.com/article/shannonkeating/rapid-onset-gender-dysphoria-flawed-methods-transgender">not a real phenomenon</a>. The critical flaw in the idea is so blatantly obvious that you’ve very nearly spelled it out yourself: <strong>parents</strong> described an “unusual” pattern of behavior. Not the children themselves, not psychologists, not therapists. Parents. Parents who are upset that their children are coming out as trans, who are searching for some external factor to blame so they can rest assured that their children have simply been taken advantage of by some nefarious force.</p>
<p>I remember this all quite well from the 90s, except then it was about homosexuality. (A pattern begins to emerge.) There were no signs!, cry parents who punished their children for ever showing any signs, thus swiftly teaching them to put on a good act. It must be the media. It must be the evil other gays somehow influencing my poor child, who otherwise <em>would</em> be straight, like I want them to be.</p>
<p>The only difference is that this time it’s been given an acronym to lend it some veneer of credibility. But it’s not a clinical diagnosis; it’s a study of the feelings of <em>parents</em> who were caught off guard and are searching for an explanation other than “my child is trans”. Even <a href="proxy.php?url=https://journals.plos.org/plosone/article?id=10.1371/journal.pone.0214157">the paper itself</a> has a preface saying the term “should not be used in a way to imply that it explains the experiences of all gender dysphoric youth”.</p>
<p>There’s no mystery to be solved here, anyway. Talk to a single queer person (who isn’t isolated due to factors beyond their control) and I’ll bet you they have disproportionately many queer friends. People who are alike tend to clump together, especially if they sense that society at large is uncomfortable with them. All that’s been observed here is that trans teenagers form friend groups, and when one of them comes out, the others feel confident enough to come out as well. And their parents don’t like it, because of a culture that includes essays like this from household names with massive platforms.</p>
<blockquote>
<p>However, her career took a similar hit to that suffered by Maya Forstater. Lisa Littman had dared challenge one of the central tenets of trans activism, which is that a person’s gender identity is innate, like sexual orientation. Nobody, the activists insisted, could ever be persuaded into being trans.</p>
</blockquote>
<p>I remember this from the 90s, too. I remember the argument having to be made that sexual orientation is fixed and absolute and predetermined — because, regardless of how true or universal that may or may not be, the alternative is to leave the door open for parents and communities to try to “fix” gay children and ostracize the gay adults who had “persuaded” them into being gay.</p>
<p>Here we go again, except the “fix” for trans youth is to merely tell them to knock it off because they’re mistaken and leave it at that.</p>
<blockquote>
<p>The argument of many current trans activists is that if you don’t let a gender dysphoric teenager transition, they will kill themselves. In an article explaining why he resigned from the Tavistock (an <span class="caps">NHS</span> gender clinic in England) psychiatrist Marcus Evans stated that claims that children will kill themselves if not permitted to transition do not ‘align substantially with any robust data or studies in this area. Nor do they align with the cases I have encountered over decades as a psychotherapist.’</p>
</blockquote>
<p>They won’t <em>necessarily</em> kill themselves, but you could throw a rock and hit a study telling you that trans folks have a shockingly high rate of suicide attempts, and the absolute number one factor that drops that rate precipitously is transition. Or you could talk to a trans person and see if they have a friend who attempted/committed suicide because they were unable to transition (yes). Or at the very least, maybe cite someone who <em>didn’t</em> resign.</p>
<p>What a shockingly insensitive thing to say.</p>
<blockquote>
<p>The writings of young trans men reveal a group of notably sensitive and clever people. The more of their accounts of gender dysphoria I’ve read, with their insightful descriptions of anxiety, dissociation, eating disorders, self-harm and self-hatred, the more I’ve wondered whether, if I’d been born 30 years later, I too might have tried to transition. The allure of escaping womanhood would have been huge. I struggled with severe <span class="caps">OCD</span> as a teenager. If I’d found community and sympathy online that I couldn’t find in my immediate environment, I believe I could have been persuaded to turn myself into the son my father had openly said he’d have preferred.</p>
</blockquote>
<p>You call them clever, but immediately turn around and suggest that they are somehow artificially trans, that they have been “persuaded” into it. Again, you express ostensible care but use it as a springboard to dismiss them and talk over them. And what of trans women, who are well aware of what womanhood entails but still prefer it? This is precisely what I mentioned as the common <span class="caps">TERF</span> rhetoric, and is why people are calling you one: you speak piteously of trans men while suggesting with every word that you know better than they do what’s good for them, while trans women are… well, who knows what that omission might imply?</p>
<blockquote>
<p>When I read about the theory of gender identity, I remember how mentally sexless I felt in youth. I remember Colette’s description of herself as a ‘mental hermaphrodite’ and Simone de Beauvoir’s words: ‘It is perfectly natural for the future woman to feel indignant at the limitations posed upon her by her sex. The real question is not why she should reject them: the problem is rather to understand why she accepts them.’</p>
<p>As I didn’t have a realistic possibility of becoming a man back in the 1980s, it had to be books and music that got me through both my mental health issues and the sexualised scrutiny and judgement that sets so many girls to war against their bodies in their teens. Fortunately for me, I found my own sense of otherness, and my ambivalence about being a woman, reflected in the work of female writers and musicians who reassured me that, in spite of everything a sexist world tries to throw at the female-bodied, it’s fine not to feel pink, frilly and compliant inside your own head; it’s <span class="caps">OK</span> to feel confused, dark, both sexual and non-sexual, unsure of what or who you are.</p>
</blockquote>
<p>At last, you spell it out. But trans men are not confused and don’t need you to save them.</p>
<blockquote>
<p>I want to be very clear here: I know transition will be a solution for some gender dysphoric people, although I’m also aware through extensive research that studies have consistently shown that between 60-90% of gender dysphoric teens will grow out of their dysphoria.</p>
</blockquote>
<p>Flat-out incorrect. I assume you’re referring to <a href="proxy.php?url=https://www.kqed.org/futureofyou/441784/the-controversial-research-on-desistance-in-transgender-youth">research</a> that the bulk (“65 to 94 percent”) of dysphoric <em>prepubescent children</em> will “grow out of it” — but if it persists beyond puberty (i.e., into their teens), it’s <a href="proxy.php?url=https://books.google.com/books?id=Np8xxP6pcdUC&pg=RA1-PT483">most likely permanent</a>.</p>
<blockquote>
<p>Again and again I’ve been told to ‘just meet some trans people.’ I have: in addition to a few younger people, who were all adorable, I happen to know a self-described transsexual woman who’s older than I am and wonderful. Although she’s open about her past as a gay man, I’ve always found it hard to think of her as anything other than a woman, and I believe (and certainly hope) she’s completely happy to have transitioned.</p>
</blockquote>
<p>Describing them as “adorable” does not fill me with confidence that you listened to anything they had to say, especially in light of your repeated attempts to cast trans boys as confused or misled.</p>
<p>I’m glad you have 1 trans friend, whose viewpoint or input you manage to not actually mention whatsoever before using her as a foothold to make another “concerned” point:</p>
<blockquote>
<p>Being older, though, she went through a long and rigorous process of evaluation, psychotherapy and staged transformation. The current explosion of trans activism is urging a removal of almost all the robust systems through which candidates for sex reassignment were once required to pass.</p>
</blockquote>
<p>If you would “just meet some trans people”, you would know that the long and rigorous process is torture. Quite regularly I see tweets — from folks in the <span class="caps">UK</span> especially — about having to wait for up to a year or more just to see a gender therapist <em>once</em>, after which they have to wait <em>even longer</em> to even begin hormones. In the <span class="caps">US</span>, I’ve read no end of anecdotes from people who have to perform the right “kind” of transness to convince a therapist to write them a referral letter, after who knows how many sessions. And this is, quite often, after years of <em>internal</em> debate and questioning. Years and years of their lives lost forever.</p>
<p>All of this is predicated, once again, on the idea that trans people just don’t know what’s good for themselves.</p>
<blockquote>
<p>A man [sic] who intends to have no surgery and take no hormones may now secure himself [sic] a Gender Recognition Certificate and be a woman in the sight of the law. Many people aren’t aware of this.</p>
</blockquote>
<p>She would <a href="proxy.php?url=https://www.gov.uk/apply-gender-recognition-certificate">need</a> a formal diagnosis and to have lived as a woman for at least two years. At least as written, a cis man cannot simply show up and get an F stamped on his passport. I don’t even know what possible purpose that would serve.</p>
<blockquote>
<p>We’re living through the most misogynistic period I’ve experienced. Back in the 80s, I imagined that my future daughters, should I have any, would have it far better than I ever did, but between the backlash against feminism and a porn-saturated online culture, I believe things have got significantly worse for girls. Never have I seen women denigrated and dehumanised to the extent they are now. From the leader of the free world’s long history of sexual assault accusations and his proud boast of ‘grabbing them by the pussy’, to the incel (‘involuntarily celibate’) movement that rages against women who won’t give them sex, to the trans activists who declare that TERFs need punching and re-educating, men across the political spectrum seem to agree: women are asking for trouble. Everywhere, women are being told to shut up and sit down, or else.</p>
</blockquote>
<p>I cannot believe you are comparing sexual assault and incels — who have committed mass shootings! — to angry trans people tweeting anime screenshots captioned “shut up” at you. “<span class="caps">TERF</span>” doesn’t even imply a woman — the most infamous one by far <em>is a man</em>, Graham Lineham!</p>
<p>Meanwhile, <em>you</em> have — <strong>multiple times in this essay</strong> — suggested that trans boys are misled and the choices they’ve made for themselves are somehow mistakes. I know you consider them women, because your exact phrasing was to call them “girls [sic] being referred for transitioning treatment” and then reframe their choices as actually being about misogyny. What kind of feminism is it to decide you know better than people you think are women? Not even <em>decide</em>, but take for granted, speak about as though their agency never existed to be dismissed in the first place?</p>
<blockquote>
<p>I’ve read all the arguments about femaleness not residing in the sexed body, and the assertions that biological women don’t have common experiences, and I find them, too, deeply misogynistic and regressive. It’s also clear that one of the objectives of denying the importance of sex is to erode what some seem to see as the cruelly segregationist idea of women having their own biological realities or – just as threatening – unifying realities that make them a cohesive political class. The hundreds of emails I’ve received in the last few days prove this erosion concerns many others just as much. It isn’t enough for women to be trans allies. Women must accept and admit that there is no material difference between trans women and themselves.</p>
</blockquote>
<p>Who has said that cis women don’t have common biological experiences? The issue is that most trans men and some nonbinary folks <em>also</em> have those experiences (and some cis women don’t), so if you’re going to talk about them, why not talk about <em>the experience</em> instead of saying “women” and presuming that everyone will intuit which of a dozen possible facets of womanhood you’re referring to?</p>
<p>And if the experience in question is a social one, based on <em>other people’s</em> perception of you as a woman, then guess what: loads of trans women <em>will</em> also have had those experiences.</p>
<blockquote>
<p>But, as many women have said before me, ‘woman’ is not a costume. ‘Woman’ is not an idea in a man’s head. ‘Woman’ is not a pink brain, a liking for Jimmy Choos or any of the other sexist ideas now somehow touted as progressive.</p>
</blockquote>
<p>The women saying those things, anecdotally, appear to have significant overlap with women who criticize trans women for not “looking” female enough. Or who, sadly, misidentify cis women <em>as</em> trans women for not “looking” female enough. You know, that refined <em>classical</em> sexism.</p>
<p>If trans women wear dresses, they’re treating womanhood as a costume; if they don’t, they’re faking it.</p>
<blockquote>
<p>Moreover, the ‘inclusive’ language that calls female people ‘menstruators’ and ‘people with vulvas’ strikes many women as dehumanising and demeaning. I understand why trans activists consider this language to be appropriate and kind, but for those of us who’ve had degrading slurs spat at us by violent men, it’s not neutral, it’s hostile and alienating.</p>
</blockquote>
<p>Clearly you don’t understand, as no one is blanket referring to “female people” as “menstruators”. The current kerfuffle <em>started</em> because you commented on an article titled “Creating a more equal post-<span class="caps">COVID</span>-19 world for people who menstruate”. It used that phrasing <em>because it was about menstruation</em> (and was written by three women). The only person in this whole mess who has tried to reduce women to their body parts is <em>you</em>, in your initial tweet, insisting that menstruation is a uniquely defining feature of womanhood.</p>
<p>Moreover, the article is about addressing a women’s health and women’s rights issue, and it mentions women frequently, but your only response was to <em>criticize the title</em> for trying to include the very people — trans men — that you keep trampling in this essay. I find your choice of priorities increasingly alarming.</p>
<blockquote>
<p>If you could come inside my head and understand what I feel when I read about a trans woman dying at the hands of a violent man, you’d find solidarity and kinship. I have a visceral sense of the terror in which those trans women will have spent their last seconds on earth, because I too have known moments of blind fear when I realised that the only thing keeping me alive was the shaky self-restraint of my attacker.</p>
<p>I believe the majority of trans-identified people not only pose zero threat to others, but are vulnerable for all the reasons I’ve outlined. Trans people need and deserve protection. Like women, they’re most likely to be killed by sexual partners. Trans women who work in the sex industry, particularly trans women of colour, are at particular risk. Like every other domestic abuse and sexual assault survivor I know, I feel nothing but empathy and solidarity with trans women who’ve been abused by men.</p>
<p>So I want trans women to be safe. At the same time, I do not want to make natal girls and women less safe. When you throw open the doors of bathrooms and changing rooms to any man who believes or feels he’s a woman – and, as I’ve said, gender confirmation certificates may now be granted without any need for surgery or hormones – then you open the door to any and all men who wish to come inside. That is the simple truth.</p>
</blockquote>
<p>I’m sorry for what you went through, but these few paragraphs horrify me. You understand and describe in vivid detail what some of these women go through, how their lives end, how at risk they are, and then immediately segue into how those women should not be given shelter — hell, not even just shelter, but <em>a place to pee</em> — because someone else might hypothetically abuse it.</p>
<p>I must be missing something, because this has never made sense to me. People who commit sexual assault are not especially interested in following the rules, so how is adding another rule meant to dissuade them from this contrived scheme? If someone is around to police who goes into the bathroom, why could that same person not instead intervene if someone tries to cause harm?</p>
<p>Anyway, what do you propose instead? You never say, which seems deeply at odds with your desire for trans women to be safe. The only alternative I ever hear involves checking identification and chromosomal analysis and all kinds of other absurdity — which is clearly aimed at trans folks and not nefarious men. Are you fine with the status quo, which is that trans people <em>already</em> use whatever bathroom they find most appropriate? Or do you think your trans woman friend should be forced into the men’s room, surrounded by men? Without saying one way or the other, you’re actively encouraging fear and hostility towards people who <em>just want to pee</em> — and not just towards trans people, but towards anyone who doesn’t “look female enough”.</p>
<blockquote>
<p>On Saturday morning, I read that the Scottish government is proceeding with its controversial gender recognition plans, which will in effect mean that all a man needs to ‘become a woman’ is to say he’s one. To use a very contemporary word, I was ‘triggered’. Ground down by the relentless attacks from trans activists on social media, when I was only there to give children feedback about pictures they’d drawn for my book under lockdown, I spent much of Saturday in a very dark place inside my head, as memories of a serious sexual assault I suffered in my twenties recurred on a loop. That assault happened at a time and in a space where I was vulnerable, and a man capitalised on an opportunity. I couldn’t shut out those memories and I was finding it hard to contain my anger and disappointment about the way I believe my government is playing fast and loose with womens and girls’ safety.</p>
</blockquote>
<p>Why did you take it out on the very people you just said you also want to be safe? Why did you take it out on an article that had little to do with safety and was pushing for better health and privacy? You’ve already said you know exactly how your actions will be perceived, so the backlash this time cannot have come as a surprise.</p>
<p>There was so much opportunity here for talking about cultural expectations and gender roles, how those foster and overlook violence and aggression from boys from a young age, how a lot of societal structures still suggest that men are “owed” something by women, or how violence is more broadly glorified in Western culture. As a world-renowned author who’s done extensive feminist research, you could surely make an impact.</p>
<p>Instead, you decided to hurt people.</p>
<blockquote>
<p>Late on Saturday evening, scrolling through children’s pictures before I went to bed, I forgot the first rule of Twitter – never, ever expect a nuanced conversation – and reacted to what I felt was degrading language about women. I spoke up about the importance of sex and have been paying the price ever since. I was transphobic, I was a cunt, a bitch, a <span class="caps">TERF</span>, I deserved cancelling, punching and death. You are Voldemort said one person, clearly feeling this was the only language I’d understand.</p>
</blockquote>
<p>You offered absolutely no nuance yourself, and this essay has carefully weaved around it the whole time as well. You, a straight person, co-opted the gay community’s struggle so you could wield it as a club against trans people — after tossing them Dumbledore as a token afterthought — despite having ties yourself to an <span class="caps">MP</span> who has actively tried to erode gay rights.</p>
<p>But yes, let us talk about Harry Potter and how it reflects your values. <em>Zero</em> non-heterosexual characters mentioned within the canon. But more of interest: where are the women? The main character, a boy; his mentor and the primary authority figure, a man; the teacher he’s at odds with, a man; the rival and entourage, all boys; his best friend, a boy; the awkward coward who gets a late redemption arc, a boy; the primary antagonist, a man; the sympathetic adult confidant, a man; the rediscovered long-lost family member, a man; the endlessly regenerating Defense Against the Dark Arts teachers, all men except for the cartoon villain Umbridge. The Weasleys have seven children; <em>six</em> are boys. Two of the Hogwarts founders are men, and two women… ah, but the men are the founders of the two plot-important houses. Vernon is clearly the head of the Dursley family, and their only child is a boy. On it goes.</p>
<p>Girls can aspire to be the nerd no one likes (hey, that’s me!), the insane woman no one believes, the abusive monster, the nurse with no personality, or one of a handful of love interests. McGonagall is extremely cool and can turn into a cat, I grant you. And I think there was someone named Bellatrix? But wasn’t she a Death Eater?</p>
<p>I don’t claim to be an expert on your series; on the contrary, I read them casually when they came out and haven’t revisited them since. This is the cast that left an impression on me. I have published half-hour video games with more female characters than I can name off the top of my head from the entire Harry Potter canon. Where was your concern for uplifting girls throughout the decade you spent writing the most popular book series in the history of the human race? Where was your interest in the well-being of gay teens as you dedicated untold pages to descriptions of wizard football?</p>
<p>I hope that’s enough nuance.</p>
<blockquote>
<p>It would be so much easier to tweet the approved hashtags – because of course trans rights are human rights and of course trans lives matter – scoop up the woke cookies and bask in a virtue-signalling afterglow. There’s joy, relief and safety in conformity. As Simone de Beauvoir also wrote, “… without a doubt it is more comfortable to endure blind bondage than to work for one’s liberation; the dead, too, are better suited to the earth than the living.”</p>
</blockquote>
<p><span class="dquo">“</span>Virtue signalling” is not in itself a bad thing; it is literally the indication to others of what our values are, so others know what we believe and how we are likely to treat them. Your essay still signals your virtues, as does mine.</p>
<p><span class="dquo">“</span>Of course” trans rights are human rights? I cannot even tell if this is meant to be serious or sarcastic, with how much seething resentment you’ve wrapped it in. Do you also consider your supposed support of lesbians to be “conformity”, since that’s no longer an especially controversial stance?</p>
<p>This is all outright reactionary rhetoric and you know it. You are using the very same catchphrases that the incels you so revile use when justifying their hatred for women.</p>
<blockquote>
<p>Huge numbers of women are justifiably terrified by the trans activists; I know this because so many have got in touch with me to tell their stories. They’re afraid of doxxing, of losing their jobs or their livelihoods, and of violence.</p>
</blockquote>
<p>Who is doxxing people? I tried to look into this and instead found a list of <span class="caps">TERF</span> websites with a prominent warning that they track and doxx and harass trans people; the Rational Wiki asserting that TERFs engage in doxxing; and <a href="proxy.php?url=https://twitter.com/caseyexplosion/status/1214904615738761216">this second-hand account</a> that an ex-<span class="caps">TERF</span> was “threatened with doxing” by her own allies and “kept in a perpetual state of fear”.</p>
<p>And who on earth is sinking to violence over this? I find e.g. the “photo with a gun pointed at the viewer” phenomenon pretty distasteful, but it doesn’t seem to be unique to this issue, it’s not an especially credible threat of violence, and it’s the closest to actual violence I’ve ever heard of here. Surely, if anyone had come to blows, we’d never hear the end of it?</p>
<p>I note that Forstater’s contract wasn’t renewed because, as best as we can tell, she made her coworkers uncomfortable and the work environment hostile. Meanwhile, trans people can be (and are) fired for simply <em>existing</em>. Citing this as a fear people have of trans people, as though they were some large shadowy conspiracy, feels fairly tasteless.</p>
<blockquote>
<p>But endlessly unpleasant as its constant targeting of me has been, I refuse to bow down to a movement that I believe is doing demonstrable harm in seeking to erode ‘woman’ as a political and biological class and offering cover to predators like few before it. I stand alongside the brave women and men, gay, straight and trans, who’re standing up for freedom of speech and thought, and for the rights and safety of some of the most vulnerable in our society: young gay kids, fragile teenagers, and women who’re reliant on and wish to retain their single sex spaces. Polls show those women are in the vast majority, and exclude only those privileged or lucky enough never to have come up against male violence or sexual assault, and who’ve never troubled to educate themselves on how prevalent it is.</p>
</blockquote>
<p>By “young gay kids” and “fragile teenagers”, are you once again obliquely referring to young trans people who you take to be merely confused? What of <em>their</em> freedom of thought, of their right to decide who they are for themselves without seeing you use them as ammunition against other people like them? What impact do you think that will have on them, exactly?</p>
<p>Falling back on “freedom of speech” to defend one’s own hurtful speech is another reactionary talking point; when you cannot defend your speech on its own merits, you can only defend that it is not literally illegal to say.</p>
<p>What polls are you finding? <a href="proxy.php?url=https://www.pinknews.co.uk/2019/04/18/two-thirds-support-trans-bathrooms-gender-identity/">26%</a> is not a vast majority, and it’s troubling that you proactively dismiss the women who disagree with you as aloof and uninformed. What kind of feminism is that?</p>
<blockquote>
<p>The one thing that gives me hope is that the women who can protest and organise, are doing so, and they have some truly decent men and trans people alongside them. Political parties seeking to appease the loudest voices in this debate are ignoring women’s concerns at their peril. In the <span class="caps">UK</span>, women are reaching out to each other across party lines, concerned about the erosion of their hard-won rights and widespread intimidation. None of the gender critical women I’ve talked to hates trans people; on the contrary. Many of them became interested in this issue in the first place out of concern for trans youth, and they’re hugely sympathetic towards trans adults who simply want to live their lives, but who’re facing a backlash for a brand of activism they don’t endorse. The supreme irony is that the attempt to silence women with the word ‘<span class="caps">TERF</span>’ may have pushed more young women towards radical feminism than the movement’s seen in decades.</p>
</blockquote>
<p>Absolute bullshit. You’ve consistently brushed off or spoken for women and trans men who disagree with you in this post alone, but frame your own stance as though it were shared by all women. Two women you’ve mentioned by name and made a point of supporting — Maya Forstater and Magdalen Berns — have said some <em>astonishingly</em> cruel things about trans people as blanket remarks, so I can only interpret their “non-hate” in the same way as people repeatedly told my younger self that they loved me but I would burn for all eternity if I kissed both boys and girls. If their “concern” for trans youth is anything like yours, then they’re only interested in trying to berate trans youth into not wanting to be trans any more — yet again, no different from how homophobia played out.</p>
<p>And, hang on, they’re hugely sympathetic towards trans adults who’re facing backlash? You must be joking. They — and you — <strong><span class="caps">ARE</span></strong> the backlash! What good is sympathy from the very people who are deliberately hurting you?</p>
<blockquote>
<p>The last thing I want to say is this. I haven’t written this essay in the hope that anybody will get out a violin for me, not even a teeny-weeny one. I’m extraordinarily fortunate; I’m a survivor, certainly not a victim. I’ve only mentioned my past because, like every other human being on this planet, I have a complex backstory, which shapes my fears, my interests and my opinions. I never forget that inner complexity when I’m creating a fictional character and I certainly never forget it when it comes to trans people.</p>
</blockquote>
<p>You’ve done so multiple times in this essay alone, and your heroes do it on a pretty consistent basis. What an insult to everyone who read this.</p>
<blockquote>
<p>All I’m asking – all I want – is for similar empathy, similar understanding, to be extended to the many millions of women whose sole crime is wanting their concerns to be heard without receiving threats and abuse.</p>
</blockquote>
<p>In the entirety of this essay, you didn’t even mention a single concrete concern. You did some vague fearmongering about how a cis man could get a piece of paper saying he’s a woman, and that’s all. Meanwhile, you managed to repeatedly misgender and patronize trans boys; paint trans adults as a nefarious political movement trying to “persuade” children; cite multiple people who’ve been fiercely nasty towards trans people as a whole, while avoiding mentioning what they actually did so you could frame them as innocent victims; invoke multiple homophobic and reactionary tropes with a quick coat of paint slapped on top; present “parents who wish their children were cis” as though it were a diagnosed phenomenon; and generally checked off every possible <span class="caps">TERF</span> talking point while smiling kindly the whole time.</p>
<p>You’re saying things you know are actively hurtful in the name of preventing a hypothetical harm that is so nebulous you can’t even describe it.</p>
<p>This sucks.</p>Star Anise Chronicles: Oh No Wheres Twig??2020-05-10T21:54:00-07:002020-05-10T21:54:00-07:00Eeveetag:eev.ee,2020-05-10:/blog/2020/05/10/star-anise-chronicles-oh-no-wheres-twig/<div class="prose-full-illustration">
<img alt="Title and logo for the game" src="proxy.php?url=https://eev.ee/media/updates/anise-wheres-twig.png"/>
</div>
<p>🔗 <a href="proxy.php?url=https://eevee.itch.io/anise-wheres-twig"><strong>Play it</strong> on itch.io</a><br/>
🔗 <a href="proxy.php?url=https://www.lexaloffle.com/bbs/?pid=76397"><strong>Play it</strong> on the <span class="caps">PICO</span>-8 <span class="caps">BBS</span></a> (where you can also download the cart and view the source code)</p>
<p><em>(I originally drafted this just after publishing the game, but then decided to start a <a href="proxy.php?url=https://eev.ee/blog/2020/11/30/gamedev-from-scratch-0-groundwork/">whole series about its development</a> and wasn’t sure what to do with this! But it’s solid and serves a different purpose, so here it is.)</em></p>
<p>It’s been a while, but I made another <a href="proxy.php?url=https://www.lexaloffle.com/pico-8.php"><span class="caps">PICO</span>-8</a> game! It’s a little platformer with light puzzling, where you help Star Anise find his best friend Branch Commander Twig. It’s only half an hour long at worst, and it’s even playable on a phone!</p>
<p>This is the one-and-a-halfth entry in the Star Anise Chronicles series, which after several false starts, finally kicked off over Christmas with a… uh… <a href="proxy.php?url=https://eevee.itch.io/anise-escape-despair">interactive fiction game</a>. Expect the series to continue with even more whiplash-inducing theme shifts.</p>
<p>More technical considerations will go in the “gamedev from scratch” series, but read on for some overall thoughts on the design. Both contain <strong>spoilers</strong>, of course, so I do urge you to play the game first.</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/updates/anise-wheres-twig.png" alt="Title and logo for the game">
</div>
<p>🔗 <a href="proxy.php?url=https://eevee.itch.io/anise-wheres-twig"><strong>Play it</strong> on itch.io</a><br />
🔗 <a href="proxy.php?url=https://www.lexaloffle.com/bbs/?pid=76397"><strong>Play it</strong> on the <span class="caps">PICO</span>-8 <span class="caps">BBS</span></a> (where you can also download the cart and view the source code)</p>
<p><em>(I originally drafted this just after publishing the game, but then decided to start a <a href="proxy.php?url=https://eev.ee/blog/2020/11/30/gamedev-from-scratch-0-groundwork/">whole series about its development</a> and wasn’t sure what to do with this! But it’s solid and serves a different purpose, so here it is.)</em></p>
<p>It’s been a while, but I made another <a href="proxy.php?url=https://www.lexaloffle.com/pico-8.php"><span class="caps">PICO</span>-8</a> game! It’s a little platformer with light puzzling, where you help Star Anise find his best friend Branch Commander Twig. It’s only half an hour long at worst, and it’s even playable on a phone!</p>
<p>This is the one-and-a-halfth entry in the Star Anise Chronicles series, which after several false starts, finally kicked off over Christmas with a… uh… <a href="proxy.php?url=https://eevee.itch.io/anise-escape-despair">interactive fiction game</a>. Expect the series to continue with even more whiplash-inducing theme shifts.</p>
<p>More technical considerations will go in the “gamedev from scratch” series, but read on for some overall thoughts on the design. Both contain <strong>spoilers</strong>, of course, so I do urge you to play the game first.</p>
<hr />
<p>The first attempt at a Star Anise game was two years ago, in early 2018. The idea was to make a Metroidvania where Star Anise had a bunch of guns that shot cat-themed projectiles, obtained a couple other cat-themed powers, and made a total mess of a serious plot happening in the background while he ran around collecting garbage.</p>
<p>After finishing up the Steam release of Cherry Kisses last month, we decided that our next game should be that one, which would now be Star Anise 2 (since i’d already released a Star Anise 1 some months ago). We have, uh, already altered these plans, but that’s the background.</p>
<p>I don’t really know why I started on this game. I guess there’s some element of stress to working on a project <em>with</em> someone, even if that someone is Ash (my spouse), and especially if I’m supposed to be driving it forward. I have to tell someone what to do, and then if I don’t like the result I have to ask them to fix it, and a lot of tiny design questions are out of my control anyway, and all of this is happening on someone else’s schedule, and I have to convey all the project state that’s in my head in a complicated non-verbal form, and… all of those things are a constant low-level source of stress.</p>
<p>So I guess we’d just finished a game that I’d designed, and it was looking like we were about to start a sizable project where I was the design lead <em>again</em>, and I wanted to make something I could finish by myself as an interlude.</p>
<p>And so I sat down with a teeny tiny tool to make a teeny tiny version of what I expected would be our next game.</p>
<h2 id="design"><a class="toclink" href="proxy.php?url=#design">Design</a></h2>
<p>The basics were obvious: run, jump, land. I gave Star Anise little landing particles early on — they’re in the bigger prototype, I love landing puffs in general, and having them be stars adds <em>so much</em> silly personality.</p>
<p>I knew I wanted to have multiple abilities you collect, since that’s the heart of Metroidventures. I briefly considered giving Star Anise a gun, as in the prototype, but gave up on that pretty early. I would’ve had to sprite a gun, a projectile, a projectile explosion, enemies, enemy attacks, enemy death frames…</p>
<p>Don’t get me wrong; I have no problem with drawing all of that. The concern was that <span class="caps">PICO</span>-8 has a <em>very</em> limited amount of space for sprites — in the configuration I was using, 128 sprites of 8×8 pixels each. Star Anise himself takes up 9, even with some clever reuse for his walking animation. The star puff takes 4. The common world tile, plus versions for edges and corners, takes up 9. That’s 22 sprites already, more than 17% of the space I have, for <em>absolutely nothing</em> besides jumping around on solid ground. I would have to keep it simple.</p>
<p>That led me to the first two powers, both borrowed from the prototype:</p>
<ul>
<li>
<p><span class="caps">AOWR</span> starts conversation with NPCs and opens doors. I can’t really take any creative credit here, since these are both things Anise attempts to do with aowrs in real life.</p>
</li>
<li>
<p>Papping activates levers and knocks over glasses of liquid. Anise only does one of those in real life. (In the prototype, this is a <em>gun</em> — which shoots pawprint-shaped projectiles — but I’d already been thinking about making it a “melee” ability first.)</p>
</li>
</ul>
<p>I <em>adore</em> both of these abilities. I think they both turn some common <span class="caps">UI</span> tropes on their heads. NPCs, doors, and levers are all things you usually interact with by pressing some generic “interact” button, but <em>hitting</em> a lever (and meowing at a door) adds some physicality to the action — you’re actually <em>doing</em> something, not just making it go.</p>
<p>And pressing A to talk to an <span class="caps">NPC</span> doesn’t really make <em>any sense at all</em>! Consider: almost universally, even in games where the player character speaks, pressing A to start a conversation leads off with <em>the <span class="caps">NPC</span></em> talking. So what the hell did you actually do? What does pressing A represent actually <em>doing</em> that results in someone else casually starting a conversation with you, seemingly unprompted? I have no idea! It’s nonsense! But Anise meows at me all the time and I always respond to him, which is perfectly sensible.</p>
<p>The third power, telepawt, is a little newer. We’d conceived a cat teleporting power pretty recently, but it was more involved and required some big environmental props. I realized pretty quickly that I couldn’t possibly do much of interest on the tiny <span class="caps">PICO</span>-8 screen (16 × 16 tiles), but I do like teleporting abilities! I briefly considered ripping off Axiom Verge, but I’ve already done that in fox flux, and the physics are a little involved… and then, lo, inspiration! Combine the two ideas: teleport great distances, but in a controlled and predictable way, by teleporting to the point on the opposite side of the screen. It felt like a very 8-bit kind of power, and I could already imagine a few ways to hide stuff with it, so off I went.</p>
<p>And that seemed like a reasonable progression. A way to talk (and progress through doors), a way to interact with objects, and a way to move around. I decided about halfway through development to make jumping a faux powerup as well; it stretches out the progression a bit more by making you walk past potential jumps and then come back to them later, which is important when I don’t have much map space to work with.</p>
<p>I’d originally planned for items to be separate from abilities, but ran into a couple problems, the worst of which was that I really didn’t have much screen space for sprinkling more items around. I ended up turning items into abilities in their own right, which I think was an improvement overall; now you can crinkle the plastic bag wherever you want, for example.</p>
<p>The game deliberately doesn’t try to explain itself; <span class="caps">PICO</span>-8 only has six buttons, and four of them are a d-pad, so I figured button-mashing (as in ye olde <span class="caps">NES</span> days) would get everyone through. Still, several players were confused about how to jump (and possibly gave up before even acquiring jump?), and one didn’t realize you could switch abilities, despite the up/down arrowheads on the ability box. Not sure what to learn from this.</p>
<h2 id="the-map"><a class="toclink" href="proxy.php?url=#the-map">The map</a></h2>
<p>I struggled a bit with the map. <span class="caps">PICO</span>-8 has a built-in map editor with enough space for 32 screen-sized rooms (arranged in an 8 × 4 grid), which it turns out is <em>not very many</em>. I also very much did not want the game space to be confined to exactly that size of rectangle, so I knew I’d have to do some funky stuff with room connections. (Armed with that power, I ended up making loops and other kinds of non-Euclidean geometry, but hey that’s plenty appropriate for an imaginary moon.)</p>
<p>The bigger problem was designing the rooms <em>outside</em> of the <span class="caps">PICO</span>-8 map editor. I tried sketching in Krita, and then on paper, but kept running into the same two problems: it was tedious to rearrange rooms, and I didn’t have a good sense of how much space was available per room.</p>
<p>I found a novel solution: I wrote a Python script to export the map to a <span class="caps">PNG</span>, opened it in Aseprite, and edited it there — with each pixel representing a tile and the grid size set to 16. Now I knew exactly how much space I had, and rearranging rooms was easy: double-clicking a cell selects it, and holding Alt while dragging a selection snaps it to the grid. Here’s the beginning part of the game, screenshotted directly from Aseprite at 400% zoom:</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/updates/anise-wheres-twig-mapdesign.png" alt="A very pixellated map, with bright pink lines to indicate odd connections">
</div>
<p>When it came time to pack it all back into a rectangle, I copied the whole map, rearranged the rooms, and numbered them all so I could keep track of connections. Surprisingly, it wasn’t <em>that</em> bad a workflow.</p>
<p>The non-Euclidean map connections came in handy for packing secrets in more efficiently; most of the secret stars are off-screen, making them harder to find, but I couldn’t really afford to have a dedicated treasure room for every single one. So I crammed two treasures into the same room a few times, even though the two routes you’d take to get there are generally nowhere near each other. </p>
<p>Doors helped stretch the map out, too. It’s probably obvious if you think about it in the slightest, but doors don’t lead to different rooms; they reuse the <em>same</em> room. But some tiles only appear in the overworld, some tiles only appear in cave world, and actors (besides doors) don’t spawn in caves. That seemingly small difference was enough to make rooms vastly different in the two worlds; the most extreme case is a “crossroads” room, which you traverse vertically in the overworld but horizontally in cave world. (Honestly, I wish I’d done a bit more of this, but it works best in rooms that only have two overworld exits, and there ended up not being too many of those. Also, caves are restricted to basically just platforming, so there’s only so much variety I can squeeze out of them.)</p>
<p>Designing caves was a little trickier than you might think, since the <span class="caps">PICO</span>-8 map has no layers! If something needed to occupy a tile in the overworld, then I <em>could not</em> put something in the same place in cave world. Along with the design nightmare that is telepawt, this gave me a couple headaches.</p>
<p>I do like the cave concept a lot, though. I love parallel versions of places in games, and I have an unfinished <span class="caps">PICO</span>-8 game that’s centered around that idea taken to extremes. It’s also kind of a nod to my LÖVE games, all the way back to Neon Phase, where going indoors didn’t load another map — rooms were just another layer.</p>
<h2 id="aesthetics"><a class="toclink" href="proxy.php?url=#aesthetics">Aesthetics</a></h2>
<p>Originally, <span class="caps">PICO</span>-8 had a fixed palette of 16 colors. You could do palette swaps of various sorts, but you can’t actually change any of the colors.</p>
<p>But since I last used it, <span class="caps">PICO</span>-8 gained a “secret palette” — an <em>extra</em> 16 colors that you can request. You can’t have more than 16 colors on the screen at a time, but you <em>can</em> replace one of the existing colors with a “secret” color. There’s also an obscure way to tell <span class="caps">PICO</span>-8 to preserve the screen palette when the game finishes, which means I could effectively change the palette <em>in the sprite editor</em>. Hooray!</p>
<p>I didn’t want to completely change the palette, so I tried to keep the alterations minor. For the most part, I gave up reds and pinks for a better spread of greens, purples, and yellows. Here’s the core <span class="caps">PICO</span>-8 palette, the secret <span class="caps">PICO</span>-8 palette, and the game’s palette, respectively:</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/updates/anise-wheres-twig-palette.png" alt="A very bright palette, a softer and warmer version of the same colors, and a mix of them">
</div>
<p>I think I did a decent job of preserving the overall color aesthetic while softening the harsh contrasts of the original palette, and the cool colors really helped the mood.</p>
<p>Note that I changed the background color (color 0 isn’t drawn when it appears in a sprite) to <em>navy</em> and promoted black to a foreground color, which helped black stand out more when used as an outline or whatever. Probably the best example of this is in the logo, traced from the vector logo I made for the first Star Anise game.</p>
<p>Hmm, what else. The tiles themselves felt almost forced, if that makes sense? Like I could only draw them one way. <span class="caps">PICO</span>-8 tiles are a rinky-dink 8 pixels, and boy that is not much to work with. If I had a lot of sprite space, I could make bigger metatiles, but… I don’t, so I couldn’t. I tried a lot of variations of tiles, and what I ended up with were pretty much the only things that worked.</p>
<p>I <em>love</em> how the emoting came out. I knew I didn’t have nearly enough room for facial expressions for everyone, but I wanted to give them <em>some</em> kind of visual way to express mood, and the tiny overlays kinda fell naturally out of that. I think they add a ton of personality, especially in how everyone uses them differently.</p>
<p>I’m pretty happy with the sound design, as well. I’m an extremely amateur composer, and I wrote 90% of the music in a few hours on the start of the last day, but I actually like how it came out and I like going back to listen to it. The sound effects are, with some mild exceptions, pretty much excellent — the aowr is incredible, it has fooled other folks in the house more than once, and I knew I had it right when I had a blast just running around mashing the meow button.</p>
<p>I’m also happy with the dialogue, and hope it conveys the lunekos’ personalities in just these few interactions.</p>
<p>While writing the ending, I had to stop in mid-draft to go cry. Then I cried again when I finished it a few days later. I’ll miss you forever, <a href="proxy.php?url=https://eev.ee/blog/2019/10/26/goodbye-twigs/">Branch Commander Twig</a>.</p>
<p>If you did, thanks for playing.</p>Old CSS, new CSS2020-02-01T23:21:00-08:002020-02-01T23:21:00-08:00Eeveetag:eev.ee,2020-02-01:/blog/2020/02/01/old-css-new-css/<p>I first got into web design/development in the late 90s, and only as I type this sentence do I realize how long ago that was.</p>
<p>And boy, it was horrendous. I mean, being able to make stuff and put it online where other people could see it was pretty slick, but we did not have very much to work with.</p>
<p>I’ve been taking for granted that <em>most</em> folks doing web stuff still remember those days, or at least the decade that followed, but I think that assumption might be a wee bit out of date. Some time ago I encountered a <a href="proxy.php?url=https://twitter.com/keinegurke_/status/1162309192855822339">tweet</a> marvelling at what we had to do without <code>border-radius</code>. I still remember waiting with bated breath for it to be unprefixed!</p>
<p>But then, I suspect I also know a number of folks who only tried web design in the old days, and assume nothing about it has changed since.</p>
<p>I’m here to tell <em>all</em> of you to get off my lawn. Here’s a history of <span class="caps">CSS</span> and web design, as I remember it.</p>
<p>I first got into web design/development in the late 90s, and only as I type this sentence do I realize how long ago that was.</p>
<p>And boy, it was horrendous. I mean, being able to make stuff and put it online where other people could see it was pretty slick, but we did not have very much to work with.</p>
<p>I’ve been taking for granted that <em>most</em> folks doing web stuff still remember those days, or at least the decade that followed, but I think that assumption might be a wee bit out of date. Some time ago I encountered a <a href="proxy.php?url=https://twitter.com/keinegurke_/status/1162309192855822339">tweet</a> marvelling at what we had to do without <code>border-radius</code>. I still remember waiting with bated breath for it to be unprefixed!</p>
<p>But then, I suspect I also know a number of folks who only tried web design in the old days, and assume nothing about it has changed since.</p>
<p>I’m here to tell <em>all</em> of you to get off my lawn. Here’s a history of <span class="caps">CSS</span> and web design, as I remember it.</p>
<hr />
<p>(Please bear in mind that this post is a fine blend of memory and research, so I can’t guarantee any of it is actually correct, <em>especially</em> the bits about causality. You may want to try the <a href="proxy.php?url=https://www.w3.org/Style/CSS20/history.html"><span class="caps">W3C</span>’s history of <span class="caps">CSS</span></a>, which is considerably shorter, has a better chance of matching reality, and contains significantly less swearing.)</p>
<p>(Also, this would benefit greatly from more diagrams, but it took long enough just to <em>write</em>.)</p>
<h2 id="the-very-early-days"><a class="toclink" href="proxy.php?url=#the-very-early-days">The very early days</a></h2>
<p>In the beginning, there was no <span class="caps">CSS</span>.</p>
<p>This was very bad.</p>
<p>My favorite artifact of this era is the book that taught me <span class="caps">HTML</span>: O’Reilly’s <a href="proxy.php?url=https://isbnsearch.org/isbn/9781565924925"><span class="caps">HTML</span>: The Definitive Guide</a>, published in several editions in the mid to late 90s. The book was indeed about <em><span class="caps">HTML</span></em>, with no mention of <span class="caps">CSS</span> at all. I don’t have it any more and can’t readily find screenshots online, but here’s a page from <span class="caps">HTML</span> <span class="amp">&</span> <span class="caps">XHTML</span>: The Definitive Guide, which seems to be a revision (I’ll get to <span class="caps">XHTML</span> later) with much the same style. Here, then, is the cutting-edge web design advice of 199X:</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/2020-02-css/html-definitive-guide.png" alt="Screenshot of a plain website in IE, with plain black text on a white background with a simple image">
</div>
<p><span class="dquo">“</span><em>Clearly delineate headers and footers with horizontal rules.</em>”</p>
<p>No, that’s not a <code>border-top</code>. That’s an <code><hr></code>. The page title is almost certainly centered with, well, <code><center></code>.</p>
<p>The page uses the default text color, background, and font. Partly because this is a guidebook introducing concepts one at a time; partly because the book was printed in black and white; and partly, I’m sure, because it reflected the reality that coloring anything was a huge pain in the ass.</p>
<p>Let’s say you wanted all your <code><h1></code>s to be red, across your entire site. You had to do this:</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="p"><</span><span class="nt">H1</span><span class="p">><</span><span class="nt">FONT</span> <span class="na">COLOR</span><span class="o">=</span><span class="s">red</span><span class="p">></span>...<span class="p"></</span><span class="nt">FONT</span><span class="p">></</span><span class="nt">H1</span><span class="p">></span>
</code></pre></div>
<p>…<em>every single goddamn time</em>. Hope you never decide to switch to blue!</p>
<p>Oh, and everyone wrote <span class="caps">HTML</span> tags in all caps. I don’t remember why we all thought that was a good idea. Maybe this was before syntax highlighting in text editors was very common (read: I was 12 and using Notepad), and uppercase tags were easier to distinguish from body text.</p>
<p>Keeping your site consistent was thus something of a nightmare. One solution was to simply not style anything, which a lot of folks did. This was nice, in some ways, since browsers let you change those defaults, so you could read the Web how you wanted.</p>
<p>A clever alternate solution, which I remember showing up in a lot of Geocities sites, was to simply give every page a completely different visual style. Fuck it, right? Just do whatever you want on each new page.</p>
<p>That trend was quite possibly the height of web design.</p>
<p>Damn, I miss those days. There were no big walled gardens, no Twitter or Facebook. If you had anything to say to anyone, you had to put together your own website. It was <em>amazing</em>. No one knew what they were doing; I’d wager that the vast majority of web designers at the time were clueless hobbyist tweens (like me) all copying from other clueless hobbyist tweens. Half the Web was fan portals about Animorphs, with inexplicable splash pages warning you that their site worked best if you had a 640×480 screen. (Any 12-year-old with insufficient resolution should, presumably, buy a new monitor with their allowance.) Everyone who was cool and in the know used Internet Explorer 3, the most advanced browser, but some losers still used Netscape Navigator so you had to put a “Best in <span class="caps">IE</span>” animated <span class="caps">GIF</span> on your splash page too.</p>
<p>This was also the era of “web-safe colors” — a palette of 216 colors, where every channel was one of <code>00</code>, <code>33</code>, <code>66</code>, <code>99</code>, <code>cc</code>, or <code>ff</code> — which existed because some people still had 256-color monitors! The things we take for granted now, like 24-bit color.</p>
<p>In fact, a <em>lot</em> of stuff we take for granted now was still a strange and untamed problem space. You want to have the same navigation on every page on your website? Okay, no problem: copy/paste it onto each page. When you update it, be sure to update every page — but most likely you’ll forget some, and your whole site will become an archaeological dig into itself, with strata of increasingly bitrotted pages.</p>
<p>Much easier was to use <em>frames</em>, meaning the browser window is split into a grid and a different page loads in each section… but then people would get confused if they landed on an individual page without the frames, as was common when coming from a search engine like AltaVista. (I can’t believe I’m explaining frames, but no one has used them since like 2001. You know iframes? The “i” is for <em>inline</em>, to distinguish them from <em>regular</em> frames, which take up the entire viewport.)</p>
<p><span class="caps">PHP</span> wasn’t even called that yet, and nobody had heard of it. This weird “Perl” and “<span class="caps">CGI</span>” thing was really strange and hard to understand, and it didn’t work on your own computer, and the errors were hard to find and diagnose, and anyway Geocities didn’t support it. If you were <em>really</em> lucky and smart, your web host used Apache, and you could use its “server side include” syntax to do something like this:</p>
<div class="highlight"><pre><span></span><code><span class="linenos"> 1</span><span class="p"><</span><span class="nt">BODY</span><span class="p">></span>
<span class="linenos"> 2</span> <span class="p"><</span><span class="nt">TABLE</span> <span class="na">WIDTH</span><span class="o">=</span><span class="s">100%</span> <span class="na">BORDER</span><span class="o">=</span><span class="s">0</span> <span class="na">CELLSPACING</span><span class="o">=</span><span class="s">8</span> <span class="na">CELLPADDING</span><span class="o">=</span><span class="s">0</span><span class="p">></span>
<span class="linenos"> 3</span> <span class="p"><</span><span class="nt">TR</span><span class="p">></span>
<span class="linenos"> 4</span> <span class="p"><</span><span class="nt">TD</span> <span class="na">COLSPAN</span><span class="o">=</span><span class="s">2</span><span class="p">></span>
<span class="linenos"> 5</span> <span class="cm"><!--#include virtual="/header.html" --></span>
<span class="linenos"> 6</span> <span class="p"></</span><span class="nt">TD</span><span class="p">></span>
<span class="linenos"> 7</span> <span class="p"></</span><span class="nt">TR</span><span class="p">></span>
<span class="linenos"> 8</span> <span class="p"><</span><span class="nt">TR</span><span class="p">></span>
<span class="linenos"> 9</span> <span class="p"><</span><span class="nt">TD</span> <span class="na">WIDTH</span><span class="o">=</span><span class="s">20%</span><span class="p">></span>
<span class="linenos">10</span> <span class="cm"><!--#include virtual="/navigation.html" --></span>
<span class="linenos">11</span> <span class="p"></</span><span class="nt">TD</span><span class="p">></span>
<span class="linenos">12</span> <span class="p"><</span><span class="nt">TD</span><span class="p">></span>
<span class="linenos">13</span> (actual page content goes here)
<span class="linenos">14</span> <span class="p"></</span><span class="nt">TD</span><span class="p">></span>
<span class="linenos">15</span> <span class="p"></</span><span class="nt">TR</span><span class="p">></span>
<span class="linenos">16</span> <span class="p"></</span><span class="nt">TABLE</span><span class="p">></span>
<span class="linenos">17</span><span class="p"></</span><span class="nt">BODY</span><span class="p">></span>
</code></pre></div>
<p><em>Mwah.</em> Beautiful. Apache would see the special comments, paste in the contents of the referenced files, and you’re off to the races. The downside was that when you wanted to work on your site, all the navigation was missing, because you were doing it on your regular computer without Apache, and your web browser thought those were just regular <span class="caps">HTML</span> comments. It was impossible to install Apache, of course, because you had a <em>computer</em>, not a <em>server</em>.</p>
<p>Sadly, that’s all gone now — paved over by homogenous timelines where anything that wasn’t made this week is old news and long forgotten. The web was supposed to make information eternal, but instead, so much of it became ephemeral. I miss when virtually everyone I knew had their own website. Having a Twitter and an Instagram as your entire online presence is a poor substitute.</p>
<p>…</p>
<p>So, let’s look at the Space Jam website.</p>
<h2 id="case-study-space-jam"><a class="toclink" href="proxy.php?url=#case-study-space-jam">Case study: Space Jam</a></h2>
<p>Space Jam, if you’re not aware, is the greatest movie of all time. It documents Bugs Bunny’s extremely short-lived basketball career, playing alongside a live action Michael Jordan to save the planet from aliens for some reason. It was followed by a series of very successful and critically acclaimed <a href="proxy.php?url=https://www.talesofgames.com/related_game/barkley-shut-up-jam-gaiden/"><span class="caps">RPG</span> spinoffs</a>, which describe the fallout of the Space Jam and are extremely canon.</p>
<p>And we are truly blessed, for 24 years after it came out, its website is <a href="proxy.php?url=https://www.spacejam.com/1996/"><span class="caps">STILL</span> <span class="caps">UP</span></a>. We can explore the pinnacle of 1996 web design, right here, right now.</p>
<p>First, notice that every page of this site is a static page. Not only that, but it’s a static page ending in <code>.htm</code> rather than <code>.html</code>, because people on Windows versions before 95 were still beholden to 8.3 filenames. Not sure why that mattered in a <span class="caps">URL</span>, as if you were going to run Windows 3.11 on a Web server, but there you go.</p>
<p>The <span class="caps">CSS</span> for the splash page looks like this:</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="p"><</span><span class="nt">body</span> <span class="na">bgcolor</span><span class="o">=</span><span class="s">"#000000"</span> <span class="na">background</span><span class="o">=</span><span class="s">"img/bg_stars.gif"</span> <span class="na">text</span><span class="o">=</span><span class="s">"#ff0000"</span> <span class="na">link</span><span class="o">=</span><span class="s">"#ff4c4c"</span> <span class="na">vlink</span><span class="o">=</span><span class="s">"#ff4c4c"</span> <span class="na">alink</span><span class="o">=</span><span class="s">"#ff4c4c"</span><span class="p">></span>
</code></pre></div>
<p>Haha, just kidding! What the fuck is <span class="caps">CSS</span>? Space Jam predates it by a month. (I do see a single line in the page source, but I’m pretty sure that was added much later to style some legally obligatory policy links.)</p>
<p>Notice the extremely precise positioning of these navigation links. This feat was accomplished the same way everyone did everything in 1996: with tables.</p>
<p>In fact, tables have one functional advantage over <span class="caps">CSS</span> for layout, which was very important in those days, and not only because <span class="caps">CSS</span> didn’t exist yet. You see, you can ctrl-click to select a table <em>cell</em> and even drag around to select all of them, which shows you how the cells are arranged and functions as a super retro layout debugger. This was great because the first meaningful web debug tool, <a href="proxy.php?url=https://en.wikipedia.org/wiki/Firebug_%28software%29">Firebug</a>, wasn’t released until 2006 — a whole decade later!</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/2020-02-css/space-jam-landing-table-cells.png" alt="Screenshot of the Space Jam website with the navigation table's cells selected, showing how the layout works">
</div>
<p>The markup for this table is overflowing with inexplicable blank lines, but with those removed, it looks like this:</p>
<div class="highlight"><pre><span></span><code><span class="linenos"> 1</span><span class="p"><</span><span class="nt">table</span> <span class="na">width</span><span class="o">=</span><span class="s">500</span> <span class="na">border</span><span class="o">=</span><span class="s">0</span><span class="p">></span>
<span class="linenos"> 2</span><span class="p"><</span><span class="nt">TR</span><span class="p">></span>
<span class="linenos"> 3</span><span class="p"><</span><span class="nt">TD</span> <span class="na">colspan</span><span class="o">=</span><span class="s">5</span> <span class="na">align</span><span class="o">=</span><span class="s">right</span> <span class="na">valign</span><span class="o">=</span><span class="s">top</span><span class="p">></span>
<span class="linenos"> 4</span><span class="p"></</span><span class="nt">td</span><span class="p">></</span><span class="nt">tr</span><span class="p">></span>
<span class="linenos"> 5</span><span class="p"><</span><span class="nt">tr</span><span class="p">></span>
<span class="linenos"> 6</span><span class="p"><</span><span class="nt">td</span> <span class="na">colspan</span><span class="o">=</span><span class="s">2</span> <span class="na">align</span><span class="o">=</span><span class="s">right</span> <span class="na">valign</span><span class="o">=</span><span class="s">middle</span><span class="p">></span>
<span class="linenos"> 7</span><span class="p"><</span><span class="nt">br</span><span class="p">></span>
<span class="linenos"> 8</span><span class="p"><</span><span class="nt">br</span><span class="p">></span>
<span class="linenos"> 9</span><span class="p"><</span><span class="nt">br</span><span class="p">></span>
<span class="linenos">10</span><span class="p"><</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">"cmp/pressbox/pressboxframes.html"</span><span class="p">><</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">"img/p-pressbox.gif"</span> <span class="na">height</span><span class="o">=</span><span class="s">56</span> <span class="na">width</span><span class="o">=</span><span class="s">131</span> <span class="na">alt</span><span class="o">=</span><span class="s">"Press Box Shuttle"</span> <span class="na">border</span><span class="o">=</span><span class="s">0</span><span class="p">></</span><span class="nt">a</span><span class="p">></span>
<span class="linenos">11</span><span class="p"></</span><span class="nt">td</span><span class="p">></span>
<span class="linenos">12</span><span class="p"><</span><span class="nt">td</span> <span class="na">align</span><span class="o">=</span><span class="s">center</span> <span class="na">valign</span><span class="o">=</span><span class="s">middle</span><span class="p">></span>
<span class="linenos">13</span><span class="p"><</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">"cmp/jamcentral/jamcentralframes.html"</span><span class="p">><</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">"img/p-jamcentral.gif"</span> <span class="na">height</span><span class="o">=</span><span class="s">67</span> <span class="na">width</span><span class="o">=</span><span class="s">55</span> <span class="na">alt</span><span class="o">=</span><span class="s">"Jam Central"</span> <span class="na">border</span><span class="o">=</span><span class="s">0</span><span class="p">></</span><span class="nt">a</span><span class="p">></span>
<span class="linenos">14</span><span class="p"></</span><span class="nt">td</span><span class="p">></span>
<span class="linenos">15</span><span class="p"><</span><span class="nt">td</span> <span class="na">align</span><span class="o">=</span><span class="s">center</span> <span class="na">valign</span><span class="o">=</span><span class="s">top</span><span class="p">></span>
<span class="linenos">16</span><span class="p"><</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">"cmp/bball/bballframes.html"</span><span class="p">><</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">"img/p-bball.gif"</span> <span class="na">height</span><span class="o">=</span><span class="s">62</span> <span class="na">width</span><span class="o">=</span><span class="s">62</span> <span class="na">alt</span><span class="o">=</span><span class="s">"Planet B-Ball"</span> <span class="na">border</span><span class="o">=</span><span class="s">0</span><span class="p">></</span><span class="nt">a</span><span class="p">></span>
<span class="linenos">17</span><span class="p"></</span><span class="nt">td</span><span class="p">></span>
<span class="linenos">18</span><span class="p"><</span><span class="nt">td</span> <span class="na">align</span><span class="o">=</span><span class="s">center</span> <span class="na">valign</span><span class="o">=</span><span class="s">bottom</span><span class="p">></span>
<span class="linenos">19</span><span class="p"><</span><span class="nt">br</span><span class="p">></span>
<span class="linenos">20</span><span class="p"><</span><span class="nt">br</span><span class="p">></span>
<span class="linenos">21</span><span class="p"><</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">"cmp/tunes/tunesframes.html"</span><span class="p">><</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">"img/p-lunartunes.gif"</span> <span class="na">height</span><span class="o">=</span><span class="s">77</span> <span class="na">width</span><span class="o">=</span><span class="s">95</span> <span class="na">alt</span><span class="o">=</span><span class="s">"Lunar Tunes"</span> <span class="na">border</span><span class="o">=</span><span class="s">0</span><span class="p">></</span><span class="nt">a</span><span class="p">></span>
<span class="linenos">22</span><span class="p"></</span><span class="nt">td</span><span class="p">></span>
<span class="linenos">23</span><span class="p"></</span><span class="nt">tr</span><span class="p">></span>
<span class="linenos">24</span><span class="p"><</span><span class="nt">tr</span><span class="p">></span>
<span class="linenos">25</span><span class="p"><</span><span class="nt">td</span> <span class="na">align</span><span class="o">=</span><span class="s">middle</span> <span class="na">valign</span><span class="o">=</span><span class="s">top</span><span class="p">></span>
<span class="linenos">26</span><span class="p"><</span><span class="nt">br</span><span class="p">></span>
<span class="linenos">27</span><span class="p"><</span><span class="nt">br</span><span class="p">></span>
<span class="linenos">28</span><span class="p"><</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">"cmp/lineup/lineupframes.html"</span><span class="p">><</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">"img/p-lineup.gif"</span> <span class="na">height</span><span class="o">=</span><span class="s">52</span> <span class="na">width</span><span class="o">=</span><span class="s">63</span> <span class="na">alt</span><span class="o">=</span><span class="s">"The Lineup"</span> <span class="na">border</span><span class="o">=</span><span class="s">0</span><span class="p">></</span><span class="nt">a</span><span class="p">></span>
<span class="linenos">29</span><span class="p"></</span><span class="nt">td</span><span class="p">></span>
<span class="linenos">30</span><span class="p"><</span><span class="nt">td</span> <span class="na">colspan</span><span class="o">=</span><span class="s">3</span> <span class="na">rowspan</span><span class="o">=</span><span class="s">2</span> <span class="na">align</span><span class="o">=</span><span class="s">right</span> <span class="na">valign</span><span class="o">=</span><span class="s">middle</span><span class="p">></span>
<span class="linenos">31</span><span class="p"><</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">"img/p-jamlogo.gif"</span> <span class="na">height</span><span class="o">=</span><span class="s">165</span> <span class="na">width</span><span class="o">=</span><span class="s">272</span> <span class="na">alt</span><span class="o">=</span><span class="s">"Space Jam"</span> <span class="na">border</span><span class="o">=</span><span class="s">0</span><span class="p">></span>
<span class="linenos">32</span><span class="p"></</span><span class="nt">td</span><span class="p">></span>
<span class="linenos">33</span><span class="p"><</span><span class="nt">td</span> <span class="na">align</span><span class="o">=</span><span class="s">right</span> <span class="na">valign</span><span class="o">=</span><span class="s">bottom</span><span class="p">></span>
<span class="linenos">34</span><span class="p"><</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">"cmp/jump/jumpframes.html"</span><span class="p">><</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">"img/p-jump.gif"</span> <span class="na">height</span><span class="o">=</span><span class="s">52</span> <span class="na">width</span><span class="o">=</span><span class="s">58</span> <span class="na">alt</span><span class="o">=</span><span class="s">"Jump Station"</span> <span class="na">border</span><span class="o">=</span><span class="s">0</span><span class="p">></</span><span class="nt">a</span><span class="p">></span>
<span class="linenos">35</span><span class="p"></</span><span class="nt">td</span><span class="p">></span>
<span class="linenos">36</span><span class="p"></</span><span class="nt">tr</span><span class="p">></span>
<span class="linenos">37</span>...
<span class="linenos">38</span><span class="p"></</span><span class="nt">table</span><span class="p">></span>
</code></pre></div>
<p>That’s the first two rows, including the logo. You get the idea. Everything is laid out with <code>align</code> and <code>valign</code> on table cells; <code>rowspan</code>s and <code>colspan</code>s are used frequently; and there are some <code><br></code>s thrown in for good measure, to adjust vertical positioning by one line-height at a time.</p>
<p>Other fantastic artifacts to be found on this page include this header, which contains Apache <span class="caps">SSI</span> syntax! This must’ve quietly broken when the site was moved over the years; it’s currently hosted on Amazon S3. You know, Amazon? The bookstore?</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="p"><</span><span class="nt">table</span> <span class="na">border</span><span class="o">=</span><span class="s">0</span> <span class="na">cellpadding</span><span class="o">=</span><span class="s">0</span> <span class="na">cellspacing</span><span class="o">=</span><span class="s">0</span> <span class="na">width</span><span class="o">=</span><span class="s">488</span> <span class="na">height</span><span class="o">=</span><span class="s">60</span><span class="p">></span>
<span class="linenos">2</span><span class="p"><</span><span class="nt">tr</span><span class="p">></span>
<span class="linenos">3</span><span class="p"><</span><span class="nt">td</span> <span class="na">align</span><span class="o">=</span><span class="s">"center"</span><span class="p">></span><span class="cm"><!--#include virtual="html.ng/site=spacejam&type=movie&home=no&size=234&page.allowcompete=no"--></span><span class="p"></</span><span class="nt">td</span><span class="p">></span>
<span class="linenos">4</span><span class="p"><</span><span class="nt">td</span> <span class="na">align</span><span class="o">=</span><span class="s">"center"</span> <span class="na">width</span><span class="o">=</span><span class="s">"20"</span><span class="p">></</span><span class="nt">td</span><span class="p">></span>
<span class="linenos">5</span><span class="p"><</span><span class="nt">td</span> <span class="na">align</span><span class="o">=</span><span class="s">"center"</span><span class="p">></span><span class="cm"><!--#include virtual="html.ng/site=spacejam&type=movie&home=no&size=234"--></span><span class="p"></</span><span class="nt">td</span><span class="p">></span>
<span class="linenos">6</span><span class="p"></</span><span class="nt">tr</span><span class="p">></span>
<span class="linenos">7</span><span class="p"></</span><span class="nt">table</span><span class="p">></span>
</code></pre></div>
<p>Okay, let’s check out <a href="proxy.php?url=https://www.spacejam.com/1996/cmp/jamcentral/jamcentralframes.html">jam central</a>. I’ve used my browser dev tools to reduce the viewport to 640×480 for the authentic experience (although I’d also have lost some vertical space to the title bar, taskbar, and five or six <span class="caps">IE</span> toolbars).</p>
<p>Note the frames: the logo in the top left leads back to the landing page, cleverly saving screen space on repeating all that navigation, and the top right is a fucking ad banner which has been blocked like seven different ways. All three parts are separate pages.</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/2020-02-css/space-jam-central.png" alt="Screenshot of the Space Jam website's 'Jam Central'">
</div>
<p>Note also the utterly unreadable red text on a textured background, one of the truest hallmarks of 90s web design. “Why not put that block of text on an easier-to-read background?” you might ask. You imbecile. How would I <em>possibly</em> do that? Only the <code><body></code> has a <code>background</code> attribute! I could use a table, but tables only support solid background colors, and that would look so boring!</p>
<p>But wait, what is this new navigation widget? How are the links all misaligned like that? Is this yet another table? Well, no, although filling a table with chunks of a sliced-up image wasn’t uncommon. But this is an <em>imagemap</em>, a long-forgotten <span class="caps">HTML</span> feature. I’ll just show you the source:</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="p"><</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">"img/m-central.jpg"</span> <span class="na">height</span><span class="o">=</span><span class="s">301</span> <span class="na">width</span><span class="o">=</span><span class="s">438</span> <span class="na">border</span><span class="o">=</span><span class="s">0</span> <span class="na">alt</span><span class="o">=</span><span class="s">"navigation map"</span> <span class="na">usemap</span><span class="o">=</span><span class="s">"#map"</span><span class="p">><</span><span class="nt">br</span><span class="p">></span>
<span class="linenos">2</span>
<span class="linenos">3</span><span class="p"><</span><span class="nt">map</span> <span class="na">name</span><span class="o">=</span><span class="s">"map"</span><span class="p">></span>
<span class="linenos">4</span><span class="p"><</span><span class="nt">area</span> <span class="na">shape</span><span class="o">=</span><span class="s">"rect"</span> <span class="na">coords</span><span class="o">=</span><span class="s">"33,92,178,136"</span> <span class="na">href</span><span class="o">=</span><span class="s">"prodnotesframes.html"</span> <span class="na">target</span><span class="o">=</span><span class="s">"_top"</span><span class="p">></span>
<span class="linenos">5</span><span class="p"><</span><span class="nt">area</span> <span class="na">shape</span><span class="o">=</span><span class="s">"rect"</span> <span class="na">coords</span><span class="o">=</span><span class="s">"244,111,416,152"</span> <span class="na">href</span><span class="o">=</span><span class="s">"photosframes.html"</span> <span class="na">target</span><span class="o">=</span><span class="s">"_top"</span><span class="p">></span>
<span class="linenos">6</span><span class="p"><</span><span class="nt">area</span> <span class="na">shape</span><span class="o">=</span><span class="s">"rect"</span> <span class="na">coords</span><span class="o">=</span><span class="s">"104,138,229,181"</span> <span class="na">href</span><span class="o">=</span><span class="s">"filmmakersframes.html"</span> <span class="na">target</span><span class="o">=</span><span class="s">"_top"</span><span class="p">></span>
<span class="linenos">7</span><span class="p"><</span><span class="nt">area</span> <span class="na">shape</span><span class="o">=</span><span class="s">"rect"</span> <span class="na">coords</span><span class="o">=</span><span class="s">"230,155,334,197"</span> <span class="na">href</span><span class="o">=</span><span class="s">"trailerframes.html"</span> <span class="na">target</span><span class="o">=</span><span class="s">"_top"</span><span class="p">></span>
<span class="linenos">8</span><span class="p"></</span><span class="nt">map</span><span class="p">></span>
</code></pre></div>
<p>I assume this is more or less self-explanatory. The <code>usemap</code> attribute attaches an image map, which is defined as a bunch of clickable areas, beautifully encoded as inscrutable lists of coordinates or something.</p>
<p>And this stuff still works! This is in <span class="caps">HTML</span>! You could use it right now! Probably don’t though!</p>
<h3 id="the-thumbnail-grid"><a class="toclink" href="proxy.php?url=#the-thumbnail-grid">The thumbnail grid</a></h3>
<p>Let’s look at one more random page here. I’d love to see some photos from the film. (Wait, <em>photos</em>? Did we not know what “screenshots” were yet?)</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/2020-02-css/space-jam-photos.png" alt="Screenshot of the Space Jam website's photos page">
</div>
<p>Another frameset, but arranged differently this time.</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="p"><</span><span class="nt">body</span> <span class="na">bgcolor</span><span class="o">=</span><span class="s">"#7714bf"</span> <span class="na">background</span><span class="o">=</span><span class="s">"img/bg-jamcentral.gif"</span> <span class="na">text</span><span class="o">=</span><span class="s">"#ffffff"</span> <span class="na">link</span><span class="o">=</span><span class="s">"#edb2fc"</span> <span class="na">vlink</span><span class="o">=</span><span class="s">"#edb2fc"</span> <span class="na">alink</span><span class="o">=</span><span class="s">"#edb2fc"</span><span class="p">></span>
</code></pre></div>
<p>They did an important thing here: since they specified a background image (which is opaque), they <em>also</em> specified a background color. Without it, if the background image failed to load, the page would be white text on the default white background, which would be unreadable.</p>
<p>(That’s <em>still</em> an important thing to keep in mind. I feel like modern web development tends to assume everything will load, or sees loading as some sort of inconvenience to be worked around, but not everyone is working on a wired connection in a San Francisco office twenty feet away from a backbone.)</p>
<p>But about the page itself. Thumbnail grids are a classic problem of web design, dating all the way back to… er… well, at least as far back as Space Jam. The main issue is that you want to <em>put things next to each other</em>, whereas <span class="caps">HTML</span> defaults to stacking everything in one big column. You could put all the thumbnails inline, in a single row of (wrapping) text, but that wouldn’t be much of a grid — and you usually want each one to have some sort of caption.</p>
<p>Space Jam’s approach was to use the only real tool anyone had in their toolbox at the time: a table. It’s structured like this:</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="p"><</span><span class="nt">table</span> <span class="na">cellpadding</span><span class="o">=</span><span class="s">10</span><span class="p">></span>
<span class="linenos">2</span><span class="p"><</span><span class="nt">tr</span><span class="p">><</span><span class="nt">td</span> <span class="na">align</span><span class="o">=</span><span class="s">center</span><span class="p">><</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">"..."</span><span class="p">><</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">"..."</span><span class="p">></</span><span class="nt">a</span><span class="p">></</span><span class="nt">td</span><span class="p">></span>...<span class="p"></</span><span class="nt">tr</span><span class="p">></span>
<span class="linenos">3</span><span class="p"><</span><span class="nt">tr</span><span class="p">></span>...<span class="p"></</span><span class="nt">tr</span><span class="p">></span>
<span class="linenos">4</span><span class="p"><</span><span class="nt">tr</span><span class="p">></span>...<span class="p"></</span><span class="nt">tr</span><span class="p">></span>
<span class="linenos">5</span><span class="p"><</span><span class="nt">table</span><span class="p">></span>
</code></pre></div>
<p>A 3×3 grid of thumbnails, left to the browser to arrange. (The last image, on a row of its own, isn’t actually part of the table.) This can’t scale to fit your screen, but everyone’s screen was pretty tiny back then, so that was <em>slightly</em> less of a concern. They didn’t add captions here, but since every thumbnail is wrapped in a table cell, they easily could have.</p>
<p>This was the state of the art in thumbnail grids in 1996. We’ll be revisiting this little <span class="caps">UI</span> puzzle a few times; you can see live examples (and view source for sample markup) on a <a href="proxy.php?url=https://eev.ee/media/2020-02-css/thumbnail-grids.html#tables">separate page</a>.</p>
<p>But let’s take a moment to appreciate the size of the “full-size, full-color, internet-quality” movie screenshots on my current monitor.</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/2020-02-css/space-jam-photo-size.png" alt="Screenshot of one of the Space Jam website's full-size photos, fullscreened on my monitor">
</div>
<p>Hey, though, they’re less than 16 <span class="caps">KB</span>! That’ll only take nine seconds to download.</p>
<p>(I’m reminded of the problem of embedded <em>video</em>, which wasn’t solved until <span class="caps">HTML5</span>’s <code><video></code> tag some years later. Until then, you had to use a binary plugin, and all of them were terrible.)</p>
<p>(Oh, by the way: images within links, by default, have a link-colored border around them. Image links are <em>usually</em> self-evident, so this was largely annoying, and until <span class="caps">CSS</span> you had to disable them for every single image with <code><img border=0></code>.)</p>
<h2 id="the-regular-early-days"><a class="toclink" href="proxy.php?url=#the-regular-early-days">The regular early days</a></h2>
<p>So that’s where we started, and it sucked. If you wanted <em>any</em> kind of consistency on more than a handful of pages, your options were very limited, and they were pretty much limited to a whole lot of copying and pasting. The Space Jam website opted to, for the most part, not bother at all — as did many others.</p>
<p>Then <span class="caps">CSS</span> came along, it was a <em>fucking miracle</em>. All that inline repetition went away. You want all your top-level headings to be a particular color? No problem:</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="nt">H1</span><span class="w"> </span><span class="p">{</span>
<span class="linenos">2</span><span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="mh">#FF0000</span><span class="p">;</span>
<span class="linenos">3</span><span class="p">}</span>
</code></pre></div>
<p>Bam! You’re done. No matter how many <code><h1></code>s you have in your document, every single one of them will be eye-searing red, and you never have to think about it again. Even better, you can put that snippet in its own file and have that questionable aesthetic choice applied to <em>every page of your whole site</em> with almost no effort! The same applied to your gorgeous tiling background image, the colors of your links, and the size of the font in your tables.</p>
<p>(Just remember to wrap the contents of your <code><style></code> tags in <span class="caps">HTML</span> comments, or old browsers without <span class="caps">CSS</span> support will display them as text.)</p>
<p>You weren’t limited to styling tags en masse, either. <span class="caps">CSS</span> introduced “classes” and “IDs” to target only specifically flagged elements. A <em>selector</em> like <code>P.important</code> would only affect <code><P CLASS="important"></code>, and <code>#header</code> would only affect <code><H1 ID="header"></code>. (The difference is that IDs are intended to be unique in a document, whereas classes can be used any number of times.) With these tools, you could effectively invent your own tags, giving you a customized version of <span class="caps">HTML</span> specific to your website!</p>
<p>This was a huge leap forward, but at the time, no one (probably?) was thinking of using <span class="caps">CSS</span> to actually <em>arrange</em> the page. When <a href="proxy.php?url=https://www.w3.org/TR/2008/REC-CSS1-20080411/"><span class="caps">CSS</span> 1</a> was made a recommendation in December ‘96, it barely addressed layout at all. All it did was divorce <span class="caps">HTML</span>’s <em>existing</em> abilities from the tags they were attached to. We had font colors and backgrounds <em>because</em> <code><FONT COLOR></code> and <code><BODY BACKGROUND></code> existed. The only feature that even remotely affected where things were positioned was the <code>float</code> property, the equivalent to <code><IMG ALIGN></code>, which pulled an image to the side and let text flow around it, like in a magazine article. Hardly whelming.</p>
<p>This wasn’t too surprising. <span class="caps">HTML</span> hadn’t had any real answers for layout besides tables, and the table properties were too complicated to generalize in <span class="caps">CSS</span> and too entangled with the tag structure, so there was nothing for <span class="caps">CSS</span> 1 to inherit. It merely reduced the repetition in what we were already doing with e.g. <code><FONT></code> tags — making Web design less tedious, less error-prone, less full of noise, and much more maintainable. A pretty good step forward, and everyone happily adopted it for that, but tables remained king for arranging your page.</p>
<p>That was okay, though; all your blog really needed was a header and a sidebar, which tables could do just fine, and it wasn’t like you were going to overhaul that basic structure very often. Copy/pasting a few lines of <code><TABLE BORDER=0></code> and <code><TD WIDTH=20%></code> wasn’t nearly as big a deal.</p>
<p>For some span of time — I want to say a couple years, but time passes more slowly when you’re a kid — this was the state of the Web. Tables for layout, <span class="caps">CSS</span> for… well, <em>style</em>. Colors, sizes, bold, underline. There was even this sick trick you could do with links where they’d <em>only</em> be underlined when the mouse was <em>pointing</em> at them. Tubular!</p>
<p>(Fun fact: <span class="caps">HTML</span> <em>email</em> is still basically trapped in this era.)</p>
<p>(And here’s about where I come in, at the ripe old age of 11, with no clue what I was doing and mostly learning from other 11-year-olds who also had no clue what they were doing. But that was fine; a huge chunk of the Web was 11-year-olds making their own websites, and it was beautiful. Why would you go to a <em>business</em> website when you can take a peek into the very specific hobbies of someone on the other side of the planet?)</p>
<h2 id="the-dark-times"><a class="toclink" href="proxy.php?url=#the-dark-times">The dark times</a></h2>
<p>A year and a half later, in mid ‘98, we were gifted <a href="proxy.php?url=https://www.w3.org/TR/2008/REC-CSS2-20080411/"><span class="caps">CSS</span> 2</a>. (I <em>love</em> the background on this page, by the way.) This was a modest upgrade that addressed a few deficiencies in various areas, but most interesting was the addition of a couple positioning primitives: the <code>position</code> property, which let you place elements at precise coordinates, and the <code>inline-block</code> display mode, which let you stick an element in a line of text like you could do with images.</p>
<p>Such tantalizing fruit, just out of reach! Using <code>position</code> seemed nice, but pixel-perfect positioning was at serious odds with the fluid design of <span class="caps">HTML</span>, and it was difficult to make much of anything that didn’t fall apart on other screen sizes or have other serious drawbacks. This humble <code>inline-block</code> thing <em>seemed</em> interesting enough; after all, it solved the core problem of <span class="caps">HTML</span> layout, which is <em>putting things next to each other</em>. But at least for the moment, no browser implemented it, and it was largely ignored.</p>
<p>I can’t say for sure if it was the introduction of positioning or some other factor, but <em>something</em> around this time inspired folks to try doing layout in <span class="caps">CSS</span>. Ideally, you would <em>completely</em> divorce the structure of your page from its appearance. A website even came along to take this principle to the extreme — <a href="proxy.php?url=http://www.csszengarden.com/"><span class="caps">CSS</span> Zen Garden</a> is still around, and showcases the <em>same <span class="caps">HTML</span></em> being radically transformed into completely different designs by applying different stylesheets.</p>
<p>Trouble was, early <span class="caps">CSS</span> support was buggy as hell. In retrospect, I suspect browser vendors merely plucked the behavior off of <span class="caps">HTML</span> tags and called it a day. I’m delighted to say that RichInStyle still has <a href="proxy.php?url=http://www.richinstyle.com/bugs/">an extensive list of early browser <span class="caps">CSS</span> bugs</a> up; here are some of my favorites:</p>
<ul>
<li>
<p><span class="caps">IE</span> 3 would ignore all but the last <code><style></code> tag in a document.</p>
</li>
<li>
<p><span class="caps">IE</span> 3 ignored pseudo-classes, so <code>a:hover</code> would be treated as <code>a</code>.</p>
</li>
<li>
<p><span class="caps">IE</span> 3 and <span class="caps">IE</span> 4 treated <code>auto</code> margins as zero. Actually, I think this one might’ve persisted all the way to <span class="caps">IE</span> 6. But that was okay, because <span class="caps">IE</span> 6 also incorrectly applied <code>text-align: center</code> to block elements.</p>
</li>
<li>
<p>If you set a background image to an absolute <span class="caps">URL</span>, <span class="caps">IE</span> 3 would try to open the image in a local program, as though you’d downloaded it.</p>
</li>
<li>
<p>Netscape 4 understood an <span class="caps">ID</span> selector like <code>#id</code>, but ignored <code>h1#id</code> as invalid.</p>
</li>
<li>
<p>Netscape 4 didn’t inherit properties — including font and text color! — into table cells.</p>
</li>
<li>
<p>Netscape 4 applied properties on <code><li></code> to the list <em>marker</em>, rather than the contents.</p>
</li>
<li>
<p>If the same element has both <code>float</code> and <code>clear</code> (not unreasonable), Netscape 4 for Mac crashes.</p>
</li>
</ul>
<p>This is what we had to work with. And folks wanted to use <span class="caps">CSS</span> to <em>lay out</em> an <em>entire page</em>? Ha.</p>
<p>Yet the idea grew in popularity. It even became a sort of elitist rallying cry, a best practice used to beat other folks over the head. Tables for layout are just plain bad, you’d hear! They confuse screenreaders, they’re semantically incorrect, they interact poorly with <span class="caps">CSS</span> positioning! All of which is true, but it was a much tougher pill to swallow when the alternative was—</p>
<p>Well, we’ll get to that in a moment. First, some background on the Web landscape circa 2000.</p>
<h3 id="the-end-of-the-browser-wars-and-subsequent-stagnation"><a class="toclink" href="proxy.php?url=#the-end-of-the-browser-wars-and-subsequent-stagnation">The end of the browser wars and subsequent stagnation</a></h3>
<p>The short version is: this company Netscape had been selling its Navigator browser (to businesses; it was free for personal use), and then Microsoft entered the market with its completely free Internet Explorer browser, and <em>then</em> Microsoft had the audacity to bundle <span class="caps">IE</span> with Windows. Can you imagine? An operating system that <em>comes with</em> a browser? This was a whole big thing, <a href="proxy.php?url=https://en.wikipedia.org/wiki/United_States_v._Microsoft_Corp.">Microsoft was sued over it</a>, and they lost, and the consequence was basically nothing.</p>
<p>But it wouldn’t have mattered either way, because they’d still <em>done it</em>, and it had worked. <span class="caps">IE</span> pretty much annihilated Netscape’s market share. Both browsers were buggy as hell, and <em>differently</em> buggy as hell, so a site built exclusively against one was likely to be a big mess when viewed in the other — this meant that when Netscape’s market share dropped, web designers paid less and less attention to it, and less of the Web worked in it, and its market share dropped further.</p>
<p>Sucks for you if you don’t use Windows, I guess. Which is funny, because there was an <span class="caps">IE</span> for Mac 5.5, and it was generally <em>less</em> buggy than <span class="caps">IE</span> 6. (Incidentally, Bill Gates wasn’t so much a brilliant nerd as an aggressive and ruthless businessman who made his fortune by deliberately striving to annihilate any competition standing in his way and making computing worse overall as a result, just saying.)</p>
<p>By the time Windows <span class="caps">XP</span> shipped in mid 2001, with Internet Explorer 6 built in, Netscape had gone from a juggernaut to a tiny niche player.</p>
<p>And then, having completely and utterly dominated, Microsoft stopped. Internet Explorer had seen a release every year or so since its inception, but <span class="caps">IE</span> 6 was the last release for more than five years. It was still buggy, but that was less noticeable when there was no competition, and it was <em>good enough</em>. Windows <span class="caps">XP</span>, likewise, was good enough to take over the desktop, and there wouldn’t be another Windows for just as long.</p>
<p>The <span class="caps">W3C</span>, the group who write the standards (not to be confused with W3Schools, who are shady <span class="caps">SEO</span> leeches), also stopped. <span class="caps">HTML</span> had seen several revisions throughout the mid 90s, and then froze as <span class="caps">HTML</span> 4. <span class="caps">CSS</span> had gotten an update in only a year and a half, and then no more; the minor update <a href="proxy.php?url=https://www.w3.org/TR/CSS21/"><span class="caps">CSS</span> 2.1</a> wouldn’t hit Candidate Recommendation status until early 2004, and took another seven years to be finalized.</p>
<p>With <span class="caps">IE</span> 6’s dominance, it was as if the entire Web was frozen in time. Standards didn’t matter, because there was effectively only one browser, and whatever it did became the de facto standard. As the Web grew in popularity, <span class="caps">IE</span>’s stranglehold also made it difficult to use any platform other than Windows, since <span class="caps">IE</span> was Windows-only and it was a coin flip whether a website would actually work with any other browser.</p>
<p>(One begins to suspect that monopolies are bad. There oughta be a law!)</p>
<p>In the meantime, Netscape had put themselves in an even worse position by deciding to do a massive rewrite of their browser engine, culminating in the vastly more standards-compliant Netscape 6 — at the cost of several years away from the market while <span class="caps">IE</span> was kicking their ass. It never broke 10% market share, while <span class="caps">IE</span>’s would peak at 96%. On the other hand, the new engine was open sourced as the Mozilla Application Suite, which would be important in a few years.</p>
<p>Before we get to that, some other things were also happening.</p>
<h3 id="quirks-mode"><a class="toclink" href="proxy.php?url=#quirks-mode">Quirks mode</a></h3>
<p>All early <span class="caps">CSS</span> implementations were riddled with bugs, but one in particular is perhaps the most infamous <span class="caps">CSS</span> bug of all time: the <em>box model bug</em>.</p>
<p>You see, a box (the rectangular space taken up by an element) has several measurements: its own width and height, then surrounding whitespace called padding, then an optional border, then a margin separating it from neighboring boxes. <span class="caps">CSS</span> specifies that these properties are all additive. A box with these styles:</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="w"> </span><span class="nt">width</span><span class="o">:</span><span class="w"> </span><span class="nt">100px</span><span class="o">;</span>
<span class="linenos">2</span><span class="w"> </span><span class="nt">padding</span><span class="o">:</span><span class="w"> </span><span class="nt">10px</span><span class="o">;</span>
<span class="linenos">3</span><span class="w"> </span><span class="nt">border</span><span class="o">:</span><span class="w"> </span><span class="nt">2px</span><span class="w"> </span><span class="nt">solid</span><span class="w"> </span><span class="nt">black</span><span class="o">;</span>
</code></pre></div>
<p>…would thus be 124 pixels wide, from border to border.</p>
<p><span class="caps">IE</span> 4 and Netscape 4, on the other hand, took a different approach: they treated <code>width</code> and <code>height</code> as measuring from border to border, and they <em>subtracted</em> the border and padding to get the width of the element itself. The same box in those browsers would be 100 pixels wide from border to border, with 76 pixels remaining for the content.</p>
<p>This conflict with the spec was not ideal, and <span class="caps">IE</span> 6 set out to fix it. Unfortunately, simply making the change would mean completely breaking the design of a whole lot of websites that had previously worked in <em>both</em> <span class="caps">IE</span> and Netscape.</p>
<p>So the <span class="caps">IE</span> team came up with a very strange compromise: they declared the old behavior (along with several other major bugs) as “quirks mode” and made it the <em>default</em>. The new “strict mode” or “standards mode” had to be opted <em>into</em>, by placing a “doctype” at the beginning of your document, before the <code><html></code> tag. It would look something like this:</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="cp"><!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"></span>
</code></pre></div>
<p>Everyone had to paste this damn mess of a line at the top of every single <span class="caps">HTML</span> document for years. (<span class="caps">HTML5</span> would later simplify it to <code><!DOCTYPE html></code>.) In retrospect, it’s a really strange way to opt into correct <span class="caps">CSS</span> behavior; doctypes had been part of the <span class="caps">HTML</span> spec since way back when it was an <a href="proxy.php?url=https://tools.ietf.org/html/rfc1866"><span class="caps">RFC</span></a>. I’m guessing the idea was that, since <em>nobody</em> bothered actually including one, it was a convenient way to allow opting in without requiring proprietary extensions just to avoid behavior that had been wrong in the first place. Good for the <span class="caps">IE</span> team!</p>
<p>The funny thing is, quirks mode still exists <em>and is still the default</em> in all browsers, twenty years later! The exact quirks have varied over time, and in particular neither Chrome nor Firefox use the <span class="caps">IE</span> box model even in quirks mode, but there are still <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Mozilla/Mozilla_quirks_mode_behavior">quite a few other emulated bugs</a>.</p>
<aside class="aside--note-from-future">
<p>Hello! Eevee here, almost two years later. You may notice the preceding link is broken. Well, it seems Mozilla made the completely baffling decision to <a href="proxy.php?url=https://groups.google.com/a/mozilla.org/g/dev-platform/c/HwRoRUOuyEw/m/ZfYG7oHZDQAJ?pli=1">nuke all Mozilla-specific information from MDN</a> on the grounds that it really belongs in Firefox documentation, then failed to add it to the Firefox documentation. So some critical technical information that's also of deep historical interest, like exactly what quirks mode even <em>does</em> in Firefox, is now lost, except for the unreadable <a href="proxy.php?url=https://github.com/mdn/archived-content/blob/main/files/en-us/mozilla/mozilla_quirks_mode_behavior/index.html">archived copy</a>. This also reduces the only mention of quirks mode on MDN to <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/HTML/Quirks_Mode_and_Standards_Mode">this lone article</a>, which says very vaguely what it is but doesn't offer so much as a glimpse at what the differences actually are. What a fucking circus.</p>
</aside>
<p>Modern browsers also have “almost standards” mode, which emulates only a single quirk, perhaps the second most infamous one: if a table cell contains only a single image, the space under the baseline is removed. Under normal <span class="caps">CSS</span> rules, the image is sitting within a line of (otherwise empty) text, which requires some space reserved underneath for descenders — the tails on letters like y. Early browsers didn’t handle this correctly, and some otherwise strict-mode websites from circa 2000 rely on it — e.g., by cutting up a large image and arranging the chunks in table cells, expecting them to display flush against each other — hence the intermediate mode to keep them limping along.</p>
<p>But getting back to the past: while this was certainly a win for standards (and thus interop), it created a new problem. Since <span class="caps">IE</span> 6 dominated, and doctypes were optional, there was little compelling reason to bother with strict mode. Other browsers ended up <em>emulating</em> it, and the non-standard behavior became its own de facto standard. Web designers who cared about this sort of thing (and to our credit, there were a lot of us) made a rallying cry out of enabling strict mode, since it was the absolute barest minimum step towards ensuring compatibility with other browsers.</p>
<h3 id="the-rise-and-fall-of-xhtml"><a class="toclink" href="proxy.php?url=#the-rise-and-fall-of-xhtml">The rise and fall of XHTML</a></h3>
<p>Meanwhile, the <span class="caps">W3C</span> had lost interest in <span class="caps">HTML</span> in favor of developing <span class="caps">XHTML</span>, an attempt to redesign <span class="caps">HTML</span> with the syntax of <span class="caps">XML</span> rather than <span class="caps">SGML</span>.</p>
<p>(What on Earth is <span class="caps">SGML</span>, you ask? I don’t know. Nobody knows. It’s the grammar <span class="caps">HTML</span> was built on, and that’s the only reason anyone has heard of it.)</p>
<p>To their credit, there were some good reasons to do this at the time. <span class="caps">HTML</span> was generally hand-written (as it still is now), and anything hand-written is likely to have the occasional bugs. Browsers weren’t in the habit of rejecting buggy <span class="caps">HTML</span> outright, so they had various error-correction techniques — and, as with everything else, different browsers handled errors differently. Slightly malformed <span class="caps">HTML</span> might appear to work fine in <span class="caps">IE</span> 6 (where “work fine” means “does what you hoped for”), but turn into a horrible mess in anything else.</p>
<p>The <span class="caps">W3C</span>’s solution was <span class="caps">XML</span>, because their solution to fucking everything in the early 2000s was <span class="caps">XML</span>. If you’re not aware, <span class="caps">XML</span> takes a much more explicit and aggressive approach to error handling — if your document contains a parse error, the <em>entire document</em> is invalid. That means if you bank on <span class="caps">XHTML</span> and make a single typo somewhere, <strong>nothing at all</strong> renders. Just an error.</p>
<p>This sucked. It sounds okay on the face of things, but consider: generic <span class="caps">XML</span> is usually assembled dynamically with <em>libraries</em> that treat a document as a tree you manipulate, then turn it all into text when you’re done. That’s great for the common use of <span class="caps">XML</span> as data serialization, where your data is already a tree and much of the <span class="caps">XML</span> structure is simple and repetitive and easy to squirrel away in functions.</p>
<p><span class="caps">HTML</span> is not like that. An <span class="caps">HTML</span> document has little reliable repeating structure; even this blog post, constructed <em>mostly</em> from <code><p></code> tags, also contains surprise <code><em></code>s within body text and the occasional <code><h2></code> between paragraphs. That’s not fun to express as a tree. And this is a big deal, because server-side rendering was becoming popular around the same time, and generated <span class="caps">HTML</span> was — still is! — put together with <em>templates</em> that treat it as a text stream.</p>
<p>If <span class="caps">HTML</span> were only written as complete static documents, then <span class="caps">XHTML</span> might have worked out — you write a document, you see it in your browser, you know it works, no problem. But generating it dynamically and risking that <em>particular edge cases</em> might replace your entire site with an unintelligible browser error? That sucks.</p>
<p>It certainly didn’t help that we were just starting to hear about this newfangled Unicode thing around this time, and it was still not always clear how exactly to make that work, and one bad <span class="caps">UTF</span>-8 sequence is enough for an entire <span class="caps">XML</span> document to be considered malformed!</p>
<p>And so, after some dabbling, <span class="caps">XHTML</span> was largely forgotten. Its legacy lives on in two ways:</p>
<ul>
<li>
<p>It got us all to stop using uppercase tag names! So long <code><BODY></code>, hello <code><body></code>. <span class="caps">XML</span> is case-sensitive, you see, and all the <span class="caps">XHTML</span> tags were defined in lowercase, so uppercase tags simply would not work. (Fun fact: to this day, JavaScript APIs report <span class="caps">HTML</span> tag names in uppercase.) The increased popularity of syntax highlighting probably also had something to do with this; we weren’t all still using Notepad as we had been in 1997.</p>
</li>
<li>
<p>A bunch of folks <em>still</em> think self-closing tags are necessary. You see, <span class="caps">HTML</span> has two kinds of tags: containers like <code><p>...</p></code> and markers like <code><br></code>. Since a <code><br></code> can’t possibly contain anything, there’s no such thing as <code></br></code>. <span class="caps">XML</span>, as a generic grammar, doesn’t have this distinction; every tag <em>must</em> be closed, but as a shortcut, you can write <code><br/></code> to mean <code><br></br></code>.</p>
<p><span class="caps">XHTML</span> has been dead for years, but for some reason, I still see folks write <code><br/></code> in regular <span class="caps">HTML</span> documents. Outside of <span class="caps">XML</span>, that slash doesn’t do anything; <span class="caps">HTML5</span> has defined it for compatibility reasons, but it’s silently ignored. It’s even actively harmful, since it might lead you to believe that <code><script/></code> is an empty <code><script></code> tag — but in <span class="caps">HTML</span>, it definitely is not!</p>
</li>
</ul>
<p>I do miss one thing about <span class="caps">XHTML</span>. You could combine it with <span class="caps">XSLT</span>, the <span class="caps">XML</span> templating meta-language, to do in-browser templating (i.e., slot page-specific contents into your overall site layout) with no scripting required. It’s the <em>only</em> way that’s ever been possible, and it was cool as all hell when it worked, but the drawbacks were too severe when it didn’t. Also, <span class="caps">XSLT</span> is totally fucking incomprehensible.</p>
<h3 id="the-beginning-of-css-layout"><a class="toclink" href="proxy.php?url=#the-beginning-of-css-layout">The beginning of CSS layout</a></h3>
<p>Back to <span class="caps">CSS</span>!</p>
<p>You’re an aspiring web designer. For whatever reason, you want to try using this <span class="caps">CSS</span> thing to lay out your whole page, even though it was <em>clearly</em> intended just for colors and stuff. What do you do?</p>
<p>As I mentioned before, your core problem is <em>putting things next to each other</em>. Putting things on <em>top</em> of each other is a non-problem — that’s the normal behavior of <span class="caps">HTML</span>. The whole reason everyone uses tables is that you can slop stuff into table cells and have it laid out side-by-side, in columns.</p>
<p>Well, tables seem to be out. <span class="caps">CSS</span> 2 had added some element display modes that corresponded to the parts of a table, but to use them, you’d have to have the same three levels of nesting as real tables: the table itself, then a row, then a cell. That doesn’t seem like a huge step up, and anyway, <span class="caps">IE</span> won’t support them until the distant future.</p>
<p>There’s that <code>position</code> thing, but it seems to make things <em>overlap</em> more often than not. Hmm.</p>
<p>What does that leave?</p>
<p>Only one tool, really: <code>float</code>.</p>
<p>I said that <code>float</code> was intended for magazine-style “pull” images, which is true, but <span class="caps">CSS</span> had defined it fairly generically. In <em>principle</em>, it could be applied to any element. If you wanted a sidebar, you could tell it to float to the left and be 20% the width of the page, and you’d get something like this:</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span>+---------+
<span class="linenos">2</span>| sidebar | Hello, and welcome to my website!
<span class="linenos">3</span>| |
<span class="linenos">4</span>+---------+
</code></pre></div>
<p>Alas! Floating has the secondary behavior that text wraps around it. If your page text was ever longer than your sidebar, it would wrap around <em>underneath</em> the sidebar, and the illusion would shatter. But hey, no problem. <span class="caps">CSS</span> specified that floats don’t wrap around each other, so all you needed to do was float the body as well!</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span>+---------+ +-----------------------------------+
<span class="linenos">2</span>| sidebar | | Hello, and welcome to my website! |
<span class="linenos">3</span>| | | |
<span class="linenos">4</span>+---------+ | Here's a longer paragraph to show |
<span class="linenos">5</span> | that my galaxy brain CSS float |
<span class="linenos">6</span> | nonsense prevents text wrap. |
<span class="linenos">7</span> +-----------------------------------+
</code></pre></div>
<p>This approach worked, but its limitations were much more obvious than those of tables. If you added a footer, for example, then it would try to fit to the <em>right</em> of the body text — remember, all of that is “pull” floats, so as far as the browser is concerned, the “cursor” is still at the top. So now you need to use <code>clear</code>, which bumps an element down below all floats, to fix that. And if you made the sidebar 20% wide and the body 80% wide, then any margin between them would add to that 100%, making the page wider than the viewport, so now you have an ugly horizontal scrollbar, so you have to do some goofy math to fix that as well. If you have borders or backgrounds on either part, then it was a little conspicuous that they were different heights, so now you have to do some <em>truly</em> grotesque stuff to fix <em>that</em>. And the more conscientious authors noticed that screenreaders would read the entire sidebar before getting to the body text, which is a pretty rude thing to subject blind visitors to, so they came up with yet <em>more</em> elaborate setups to have a three-column layout with the middle column appearing first in the <span class="caps">HTML</span>.</p>
<p>The result was a design that looked nice and worked well and scaled correctly, but backed by a weird mess of <span class="caps">CSS</span>. None of what you were <em>writing</em> actually corresponded to what you <em>wanted</em> — these are major parts of your design, not one-off pull quotes! It was difficult to understand the relationship between the layout-related <span class="caps">CSS</span> and what appeared on the screen, and that would get much worse before it got better.</p>
<h3 id="thumbnail-grid-2"><a class="toclink" href="proxy.php?url=#thumbnail-grid-2">Thumbnail grid 2</a></h3>
<p>Armed with a new toy, we can improve that thumbnail grid. The original table-based layout was, even if you don’t care about tag semantics, incredibly tedious. Now we can do better!</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="p"><</span><span class="nt">ul</span> <span class="na">class</span><span class="o">=</span><span class="s">"thumbnail-grid"</span><span class="p">></span>
<span class="linenos">2</span> <span class="p"><</span><span class="nt">li</span><span class="p">><</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">"..."</span><span class="p">><</span><span class="nt">br</span><span class="p">></span>caption<span class="p"></</span><span class="nt">li</span><span class="p">></span>
<span class="linenos">3</span> <span class="p"><</span><span class="nt">li</span><span class="p">><</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">"..."</span><span class="p">><</span><span class="nt">br</span><span class="p">></span>caption<span class="p"></</span><span class="nt">li</span><span class="p">></span>
<span class="linenos">4</span> <span class="p"><</span><span class="nt">li</span><span class="p">><</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">"..."</span><span class="p">><</span><span class="nt">br</span><span class="p">></span>caption<span class="p"></</span><span class="nt">li</span><span class="p">></span>
<span class="linenos">5</span> ...
<span class="linenos">6</span><span class="p"></</span><span class="nt">ul</span><span class="p">></span>
</code></pre></div>
<p>This is the dream of <span class="caps">CSS</span>: your <span class="caps">HTML</span> contains the page data in some sensible form, and then <span class="caps">CSS</span> describes how it actually looks.</p>
<p>Unfortunately, with <code>float</code> as the only tool available to us, the results are a bit rough. This <a href="proxy.php?url=https://eev.ee/media/2020-02-css/thumbnail-grids.html#floats">new version</a> does adapt better to various screen sizes, but it requires some hacks: the cells have to be a fixed height, centering the whole grid is fairly complicated, and the grid effect falls apart entirely with wider elements. It’s becoming clear that what we wanted is something more like a table, but with a flexible number of columns. This is just faking it.</p>
<p>You also need this weird “clearfix” thing, an incantation that would become infamous during this era. Remember that a float doesn’t move the “cursor” — a fake idea I’m using, but close enough. That means that this <code><ul></code>, which is full <em>only</em> of floated elements, has no height at all. It ends exactly where it begins, with all the floated thumbnails spilling out below it. Worse, because any subsequent elements don’t have any floated <em>siblings</em>, they’ll ignore the thumbnails entirely and render normally from just below the empty “grid” — producing an overlapping mess!</p>
<p>The solution is to add a dummy element at the <em>end</em> of the list which takes up no space, but has the <span class="caps">CSS</span> <code>clear: both</code> — bumping it down below all floats. That effectively pushes the bottom of the <code><ul></code> under all the individual thumbnails, so it fits snugly around them.</p>
<p>Browsers would later support the <code>::before</code> and <code>::after</code> <span class="dquo">“</span>generated content” pseudo-elements, which let us avoid the dummy element entirely. Stylesheets from the mid-00s were often littered with stuff like this:</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="p">.</span><span class="nc">thumbnail-grid</span><span class="p">::</span><span class="nd">after</span><span class="w"> </span><span class="p">{</span>
<span class="linenos">2</span><span class="w"> </span><span class="k">content</span><span class="p">:</span><span class="w"> </span><span class="s1">''</span><span class="p">;</span>
<span class="linenos">3</span><span class="w"> </span><span class="k">display</span><span class="p">:</span><span class="w"> </span><span class="kc">block</span><span class="p">;</span>
<span class="linenos">4</span><span class="w"> </span><span class="k">clear</span><span class="p">:</span><span class="w"> </span><span class="kc">both</span><span class="p">;</span>
<span class="linenos">5</span><span class="p">}</span>
</code></pre></div>
<p>Still, it was better than tables.</p>
<h3 id="dhtml"><a class="toclink" href="proxy.php?url=#dhtml">DHTML</a></h3>
<p>As a quick aside into the world of JavaScript, the newfangled <code>position</code> property <em>did</em> give us the ability to do some layout things dynamically. I heartily oppose such heresy, not least because no one has ever actually done it right, but it was nice for some toys.</p>
<p>Thus began the era of “dynamic <span class="caps">HTML</span>” — i.e., <span class="caps">HTML</span> affected by JavaScript, a term that has fallen entirely out of favor because we can’t even make a fucking static blog without JavaScript any more. In the early days it was much more innocuous, with teenagers putting sparkles that trailed behind your mouse cursor or little analog clocks that ticked by in real time.</p>
<p>The most popular source of these things was <a href="proxy.php?url=http://www.dynamicdrive.com/">Dynamic Drive</a>, a site that miraculously still exists and probably has a bunch of toys not updated since the early 00s.</p>
<p>But if you don’t like digging, here’s an example: every year (except this year when I forgot oops), I like to add confetti and other nonsense to my blog on my birthday. I’m very lazy so I started this tradition by using <a href="proxy.php?url=http://www.schillmania.com/projects/snowstormv12_20041121a/script/snowstorm.js">this script I found somewhere</a>, originally intended for snowflakes. It works by placing a bunch of images on the page, giving them <code>position: absolute</code>, and meticulously altering their coordinates over and over.</p>
<p>Contrast this with <a href="proxy.php?url=https://c.eev.ee/PARTYMODE/">the version I wrote from scratch a couple years ago</a>, which has only a <a href="proxy.php?url=https://c.eev.ee/PARTYMODE/partymode.js">tiny bit of <span class="caps">JS</span></a> to set up the images, then lets the browser animate them with <span class="caps">CSS</span>. It’s slightly less featureful, but lets the browser do all the work, possibly even with hardware acceleration. How far we’ve come.</p>
<h2 id="web-20"><a class="toclink" href="proxy.php?url=#web-20">Web 2.0</a></h2>
<p>Dark times can’t last forever. A combination of factors dragged us towards the light.</p>
<p>One of the biggest was <a href="proxy.php?url=https://www.mozilla.org/en-US/firefox/">Firefox</a> — or, if you were cool, originally Phoenix and then Firebird — which hit 1.0 in Nov ‘04 and went on to take a serious bite out of <span class="caps">IE</span>. That rewritten Netscape 6 browser core, the heart of the Mozilla Suite, had been extracted into a standalone browser. It was quick, it was simple, it was much more standard-compliant, and absolutely none of that mattered.</p>
<p>No, Firefox really got a foothold because it had <em>tabs</em>. <span class="caps">IE</span> 6 did not have tabs; if you wanted to open a second webpage, you opened another window. It fucking sucked, man. Firefox was a miracle.</p>
<p>Firefox wasn’t the first tabbed browser, of course; the full Mozilla Suite’s browser had them, and the obscure (but scrappy!) Opera had had them for ages. But it was Firefox that took off, for various reasons, not least of which was that it didn’t have a giant fucking ad bar at the top like Opera did.</p>
<p>Designers did push for Firefox on standards grounds, of course; it’s just that that angle primarily appealed to other designers, not so much to their parents. One of the most popular and spectacular demonstrations was the <a href="proxy.php?url=https://en.wikipedia.org/wiki/Acid2">Acid2 test</a>, intended to test a variety of features of then-modern Web standards. It had the advantage of producing a cute smiley face when rendered correctly, and a <a href="proxy.php?url=https://en.wikipedia.org/wiki/File:Ieacid2.png">fucking nightmare hellscape</a> in <span class="caps">IE</span> 6. Early Firefox wasn’t perfect, but it was certainly much closer, and you could <em>see</em> it make progress until it fully passed with the release of Firefox 3.</p>
<p>It also helped that Firefox had a faster JavaScript engine, even before <span class="caps">JIT</span> caught on. Much, much faster. Like, as I recall, <span class="caps">IE</span> 6 implemented <code>getElementById</code> by iterating over the entire document, even though IDs are unique. Glance at some <a href="proxy.php?url=https://blog.jquery.com/2011/01/31/jquery-15-released/">old jQuery release announcements</a>; they usually have some performance charts, and everything else absolutely <em>dwarfs</em> <span class="caps">IE</span> 6 through 8.</p>
<p>Oh, and there was that whole thing where <span class="caps">IE</span> 6 was a giant walking security hole, especially with its native support for arbitrary binary components that only needed a “yes” click on an arcane dialog to get full and unrestricted access to your system. Probably didn’t help its reputation.</p>
<p>Anyway, with something other than <span class="caps">IE</span> taking over serious market share, even the most ornery designers couldn’t just target <span class="caps">IE</span> 6 and call it a day any more. Now there was a <em>reason</em> to use strict mode, a reason to care about compatibility and standards — which Firefox was making a constant effort to follow better, while <span class="caps">IE</span> 6 remained stagnant.</p>
<p>(I’d argue that this effect opened the door for <span class="caps">OS</span> X to make some inroads, and also for the iPhone to exist at all. I’m not kidding! Think about it; if the iPhone browser hadn’t actually worked with anything because everyone was still targeting <span class="caps">IE</span> 6, it’d basically have been a more expensive Palm. Remember, at first Apple didn’t even want native apps; it bet on the Web.)</p>
<p>(Speaking of which, Safari was released in Jan ‘03, based on a fork of the <span class="caps">KHTML</span> engine used in <span class="caps">KDE</span>’s Konqueror browser. I think I was using <span class="caps">KDE</span> at the time, so this was very exciting, but no one else really cared about <span class="caps">OS</span> X and its 2% market share.)</p>
<p>Another major factor appeared on April Fools’ Day, 2004, when Google announced Gmail. Ha, ha! A funny joke. Webmail that isn’t terrible? That’s a good one, Google.</p>
<p>Oh. Oh, fuck. Oh they’re not kidding. <em>How the fuck does this even work</em></p>
<p>The answer, as every web dev now knows, is XMLHttpRequest — named for the fact that nobody has ever once used it to request <span class="caps">XML</span>. Apparently it was invented by Microsoft for use with Exchange, then cloned early on by Mozilla, but I’m just reading this from <a href="proxy.php?url=https://en.wikipedia.org/wiki/XMLHttpRequest">Wikipedia</a> and you can do that yourself.</p>
<p>The important thing is, it lets you make an <span class="caps">HTTP</span> request from JavaScript. You could now update only <em>part</em> a page with new data, completely in the background, without reloading. <em>Nobody</em> had heard of this thing before, so when Google dropped an entire email client based on it, it was like fucking magic.</p>
<p>Arguably the whole thing was a mistake and has led to a hell future where static pages load three paragraphs of text in the background using <span class="caps">XHR</span> for no goddamn reason, but that’s a <a href="proxy.php?url=https://eev.ee/blog/2016/03/06/maybe-we-could-tone-down-the-javascript/">different post</a>.</p>
<p>Along similar lines, August 2006 saw the release of <a href="proxy.php?url=https://jquery.com/">jQuery</a>, a similar miracle. Not only did it paper over the differences between <span class="caps">IE</span>’s “JScript” APIs and the standard approaches taken by everyone else (which had been done before by other libraries), but it made it very easy to work with whole <em>groups</em> of elements at a time, something that had historically been a huge pain in the ass. Now you could fairly easily apply <span class="caps">CSS</span> all over the place from JavaScript! Which is a bad idea! But everything was so bad that we did it anyway!</p>
<p>Hold on, I hear you cry. These things are about JavaScript! Isn’t this a post about <span class="caps">CSS</span>?</p>
<p>You’re absolutely right! I mention the rise of JavaScript because I think it led directly to the modern state of <span class="caps">CSS</span>, thanks to an increase in one big factor:</p>
<h3 id="ambition"><a class="toclink" href="proxy.php?url=#ambition">Ambition</a></h3>
<p>Firefox showed us that we could have browsers that actually, like, <em>improve</em> — every new improvement on Acid2 was exciting. Gmail showed us that the Web could do more than show plain text with snowflakes in front.</p>
<p>And folks started itching to get <em>fancy</em>.</p>
<p>The problem was, browsers hadn’t really gotten any better yet. Firefox was faster in some respects, and it adhered more closely to the <span class="caps">CSS</span> spec, but it didn’t fundamentally do anything that browsers weren’t supposed to be able to do already. Only the <em>tooling</em> had improved, and that mostly affected JavaScript. <span class="caps">CSS</span> was a static language, so you couldn’t write a library to make it better. Generating <span class="caps">CSS</span> with JavaScript was a possibility, but boy oh boy is that ever a bad idea.</p>
<p>Another problem was that <span class="caps">CSS</span> 2 was only really good at styling rectangles. That was fine in the 90s, when every <span class="caps">OS</span> had the aesthetic of rectangles containing more rectangles. But now we were in the days of Windows <span class="caps">XP</span> and <span class="caps">OS</span> X, where everything was shiny and glossy and made of curvy plastic. It was a little embarrassing to have rounded corners and neatly shaded swooshes in your <em>file browser</em> and nowhere on the Web.</p>
<p>Thus began a new reign of darkness.</p>
<h3 id="the-era-of-css-hacks"><a class="toclink" href="proxy.php?url=#the-era-of-css-hacks">The era of CSS hacks</a></h3>
<p>Designers wanted a lot of things that <span class="caps">CSS</span> just could not offer.</p>
<ul>
<li>
<p>Round corners were a big one. Square corners had fallen out of vogue, and now everyone wanted buttons with round corners, since they were The Future. (Native buttons also went out of vogue, for some reason.) Alas, <span class="caps">CSS</span> had no way to do this. Your options were:</p>
<ol>
<li>
<p>Make a fixed-size background image of a rounded rectangle and put it on a fixed-size button. Maybe drop the text altogether and just make the whole thing an image. Eugh.</p>
</li>
<li>
<p>Make a <em>generic</em> background image and scale it to fit. More clever, but the corners might end up not round.</p>
</li>
<li>
<p>Make the rounded rectangle, cut out the corner and edges, and put them in a 3×3 table with the button label in the middle. Even better, use JavaScript to do this on the fly.</p>
</li>
<li>
<p>Fuck it, make your entire website one big Flash app lol</p>
</li>
</ol>
<p>Another problem was that <span class="caps">IE</span> 6 didn’t understand PNGs with 8-bit alpha; it could only correctly display PNGs with 1-bit alpha, i.e. every pixel is either fully opaque or fully transparent, like GIFs. You had to settle for jagged edges, bake a solid background color into the image, or apply various fixes that centered around this fucking garbage nonsense:</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="nt">filter</span><span class="o">:</span><span class="w"> </span><span class="nt">progid</span><span class="p">:</span><span class="nd">DXImageTransform</span><span class="p">.</span><span class="nc">Microsoft</span><span class="p">.</span><span class="nc">AlphaImageLoader</span><span class="o">(</span><span class="nt">src</span><span class="o">=</span><span class="s1">'bite-my-ass.png'</span><span class="o">);</span>
</code></pre></div>
</li>
<li>
<p>Along similar lines: gradients and drop shadows! You can’t have fancy plastic buttons without those. But here you were basically stuck with making images again.</p>
</li>
<li>
<p>Translucency was a bit of a mess. Most browsers supported the <span class="caps">CSS</span> 3 <code>opacity</code> property since very early on… except <span class="caps">IE</span>, which needed another wacky Microsoft-specific <code>filter</code> thing. And if you wanted <em>only</em> the background translucent, you’d need a translucent <span class="caps">PNG</span>, which… well, you know.</p>
</li>
<li>
<p>Since the beginning, jQuery shipped with built-in animated effects like <code>fadeIn</code>, and they started popping up all over the place. It was kind of like the Web equivalent of how every Linux user in the mid-00s (and I include myself in this) used that fucking <a href="proxy.php?url=https://youtu.be/4QokOwvPxrE?t=118">Compiz cube effect</a>.</p>
<p>Obviously you need JavaScript to trigger an element’s disappearance in most interesting cases, but using it to control the actual animation was a bit heavy-handed and put a strain on browsers. Tabbed browsing compounded this, since browsers were largely single-threaded, and for various reasons, every open page ran in the same thread.</p>
</li>
<li>
<p>Oh! Alternating background colors on table rows. This has since gone out of style, but I think that’s a shame, because <em>man</em> did it make tables easier to read. But <span class="caps">CSS</span> had no answer for this, so you had to either give every other row a class like <code><tr class="odd"></code> (hope the table’s generated with code!) or do some jQuery nonsense.</p>
</li>
<li>
<p><span class="caps">CSS</span> 2 introduced the <code>></code> child selector, so you could write stuff like <code>ul.foo > li</code> to style special lists without messing up nested lists, and <span class="caps">IE</span> 6! Didn’t! Fucking! Support! It!</p>
</li>
</ul>
<p>All those are merely aesthetic concerns, though. If you were interested in layout, well, the rise of Firefox had made your life at once much easier and much harder.</p>
<p>Remember <code>inline-block</code>? Firefox 2 actually supported it! It was buggy and hidden behind a vendor prefix, but it more or less worked, which let designers start playing with it. And then Firefox 3 supported it more or less fully, which felt miraculous. Version 3 of our <a href="proxy.php?url=https://eev.ee/media/2020-02-css/thumbnail-grids.html#inline-block">thumbnail grid</a> is as simple as a width and <code>inline-block</code>:</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="p">.</span><span class="nc">thumbnails</span><span class="w"> </span><span class="nt">li</span><span class="w"> </span><span class="p">{</span>
<span class="linenos">2</span><span class="w"> </span><span class="k">display</span><span class="p">:</span><span class="w"> </span><span class="kc">inline-block</span><span class="p">;</span>
<span class="linenos">3</span><span class="w"> </span><span class="k">width</span><span class="p">:</span><span class="w"> </span><span class="mi">250</span><span class="kt">px</span><span class="p">;</span>
<span class="linenos">4</span><span class="w"> </span><span class="k">margin</span><span class="p">:</span><span class="w"> </span><span class="mf">0.5</span><span class="kt">em</span><span class="p">;</span>
<span class="linenos">5</span><span class="w"> </span><span class="k">vertical-align</span><span class="p">:</span><span class="w"> </span><span class="kc">top</span><span class="p">;</span>
<span class="linenos">6</span><span class="p">}</span>
</code></pre></div>
<p>The general idea of <code>inline-block</code> is that the <em>inside</em> acts like a block, but the block itself is placed in regular flowing text, like an image. Each thumbnail is thus contained in a box, but the boxes all lie next to each other, and because of their equal widths, they flow into a grid. And since it’s functionally a line of text, you don’t have to work around any weird impact on the rest of the page like you had to do with floats.</p>
<p>Sure, this had some drawbacks. You couldn’t do anything with the leftover space, for example, so there was a risk of a big empty void on the right with pathological screen sizes. You still had the problem of breaking the grid with a wide cell. But at least it’s not floats.</p>
<p>One teeny problem: <span class="caps">IE</span> 6. It did <em>technically</em> support <code>inline-block</code>, but only on elements that were naturally <code>inline</code> — ones like <code><b></code> and <code><i></code>, not <code><li></code>. So, not ones you’d actually want (or think) to use <code>inline-block</code> on. Sigh.</p>
<p>Lucky for us, at some point an absolute genius discovered <code>hasLayout</code>, an internal optimization in <span class="caps">IE</span> that marks whether an element… uh… has… layout. Look, I don’t know. Basically it changes the rendering path for an element — making it <em>differently</em> buggy, like quirks mode on a per-element basis! The upshot is that the above works in <span class="caps">IE</span> 6 if you add a couple lines:</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="p">.</span><span class="nc">thumbnails</span><span class="w"> </span><span class="nt">li</span><span class="w"> </span><span class="p">{</span>
<span class="linenos">2</span><span class="w"> </span><span class="k">display</span><span class="p">:</span><span class="w"> </span><span class="kc">inline-block</span><span class="p">;</span>
<span class="linenos">3</span><span class="w"> </span><span class="k">width</span><span class="p">:</span><span class="w"> </span><span class="mi">250</span><span class="kt">px</span><span class="p">;</span>
<span class="linenos">4</span><span class="w"> </span><span class="k">margin</span><span class="p">:</span><span class="w"> </span><span class="mf">0.5</span><span class="kt">em</span><span class="p">;</span>
<span class="linenos">5</span><span class="w"> </span><span class="k">vertical-align</span><span class="p">:</span><span class="w"> </span><span class="kc">top</span><span class="p">;</span>
<span class="linenos">6</span><span class="w"> </span><span class="err">*</span><span class="n">zoom</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span>
<span class="linenos">7</span><span class="w"> </span><span class="err">*</span><span class="k">display</span><span class="p">:</span><span class="w"> </span><span class="kc">inline</span><span class="p">;</span>
<span class="linenos">8</span><span class="p">}</span>
</code></pre></div>
<p>The leading asterisks make the property invalid, so browsers should ignore the whole line… but for some reason I cannot begin to fathom, <span class="caps">IE</span> 6 ignores the asterisks and accepts the rest of the rule. (Almost any punctuation worked, including a hyphen or — my personal favorite — an underscore.) The <code>zoom</code> property is a Microsoft extension that scales stuff, with the side effect that it grants the mystical property of “layout” to the element as well. And <code>display: inline</code> <em>should</em> make each element spill its contents into one big line of text, but <span class="caps">IE</span> treats an <code>inline</code> element that has “layout” roughly like an <code>inline-block</code>.</p>
<p>And here we saw the true potential of <span class="caps">CSS</span> messes. Browser-specific rules, with deliberate bad syntax that one browser would ignore, to replicate an effect that <em>still</em> isn’t clearly described by what you’re writing. <a href="proxy.php?url=https://blog.mozilla.org/webdev/2009/02/20/cross-browser-inline-block/">Entire tutorials</a> written to explain how to accomplish something simple, like a <em>grid</em>, but have it actually work on most people’s browsers. You’d also see <code>* html</code>, <code>html > /**/ body</code>, and all kinds of other nonsense. <a href="proxy.php?url=http://browserhacks.com/">Here’s a full list!</a> And remember that “clearfix” hack from before? The <a href="proxy.php?url=https://css-tricks.com/snippets/css/clear-fix/">full version</a>, compatible with <em>every</em> browser, is a bit worse:</p>
<div class="highlight"><pre><span></span><code><span class="linenos"> 1</span><span class="p">.</span><span class="nc">clearfix</span><span class="p">:</span><span class="nd">after</span><span class="w"> </span><span class="p">{</span>
<span class="linenos"> 2</span><span class="w"> </span><span class="k">visibility</span><span class="p">:</span><span class="w"> </span><span class="kc">hidden</span><span class="p">;</span>
<span class="linenos"> 3</span><span class="w"> </span><span class="k">display</span><span class="p">:</span><span class="w"> </span><span class="kc">block</span><span class="p">;</span>
<span class="linenos"> 4</span><span class="w"> </span><span class="k">font-size</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span>
<span class="linenos"> 5</span><span class="w"> </span><span class="k">content</span><span class="p">:</span><span class="w"> </span><span class="s2">" "</span><span class="p">;</span>
<span class="linenos"> 6</span><span class="w"> </span><span class="k">clear</span><span class="p">:</span><span class="w"> </span><span class="kc">both</span><span class="p">;</span>
<span class="linenos"> 7</span><span class="w"> </span><span class="k">height</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span>
<span class="linenos"> 8</span><span class="p">}</span>
<span class="linenos"> 9</span><span class="p">.</span><span class="nc">clearfix</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">display</span><span class="p">:</span><span class="w"> </span><span class="kc">inline-block</span><span class="p">;</span><span class="w"> </span><span class="p">}</span>
<span class="linenos">10</span><span class="c">/* start commented backslash hack \*/</span>
<span class="linenos">11</span><span class="o">*</span><span class="w"> </span><span class="nt">html</span><span class="w"> </span><span class="p">.</span><span class="nc">clearfix</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">height</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="kt">%</span><span class="p">;</span><span class="w"> </span><span class="p">}</span>
<span class="linenos">12</span><span class="p">.</span><span class="nc">clearfix</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">display</span><span class="p">:</span><span class="w"> </span><span class="kc">block</span><span class="p">;</span><span class="w"> </span><span class="p">}</span>
<span class="linenos">13</span><span class="c">/* close commented backslash hack */</span>
</code></pre></div>
<p>Is it any wonder folks started groaning about <span class="caps">CSS</span>?</p>
<p>This was an era of blind copy/pasting in the frustrated hopes of making the damn thing work. Case in point: someone (I dug the original source up once but can’t find it now) had the bone-headed idea of always setting <code>body { font-size: 62.5% }</code> due to a combination of “relative units are good” and wanting to override the seemingly massive default browser font size of 16px (which, it turns out, <a href="proxy.php?url=https://www.smashingmagazine.com/2011/10/16-pixels-body-copy-anything-less-costly-mistake/">is correct</a>) and dealing with <span class="caps">IE</span> bugs. He walked it back a short time later, but the damage had been done, and now <em>thousands</em> of websites start off that way as a “best practice”. Which means if you want to change your browser’s default font size in either direction, you’re screwed — scale it down and a bunch of the Web becomes microscopic, scale it up and everything will still be much smaller than you’ve asked for, scale it up more to compensate and everything that actually respects your decision will be ginormous. At least we have better page zoom now, I guess.</p>
<p>Oh, and do remember: Stack Overflow didn’t exist yet. This stuff was passed around purely by word of mouth. If you were lucky, you knew about some of the websites about websites, like <a href="proxy.php?url=https://www.quirksmode.org/">quirks mode</a> and <a href="proxy.php?url=https://meyerweb.com/">Eric Meyer’s website</a>.</p>
<p>In fact, check out Meyer’s <a href="proxy.php?url=https://meyerweb.com/eric/css/edge/index.html">css/edge</a> site for some wild examples of stuff folks were doing, even with just <span class="caps">CSS</span> 1, as far back as 2002. I still think <a href="proxy.php?url=https://meyerweb.com/eric/css/edge/complexspiral/demo.html">complexspiral</a> is pure genius, even though you could do it nowadays with <code>opacity</code> and just one image. The approach in <a href="proxy.php?url=https://meyerweb.com/eric/css/edge/raggedfloat/demo.html">raggedfloat</a> wouldn’t get native support in <span class="caps">CSS</span> until a few years ago, with <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/shape-outside"><code>shape-outside</code></a>! He also brought us <a href="proxy.php?url=https://meyerweb.com/eric/tools/css/reset/"><span class="caps">CSS</span> reset</a>, eliminating differences between browsers’ default styles.</p>
<p>(I cannot understate how much of a <span class="caps">CSS</span> <em>pioneer</em> Eric Meyer is. When his young daughter Rebecca died six years ago, she was uniquely immortalized with her own <span class="caps">CSS</span> color name, <a href="proxy.php?url=https://meyerweb.com/eric/thoughts/2014/06/19/rebeccapurple/"><code>rebeccapurple</code></a>. That’s how highly the Web community thinks of him. Also I have to go cry a bit over that story now.)</p>
<h2 id="the-future-arrives-gradually"><a class="toclink" href="proxy.php?url=#the-future-arrives-gradually">The future arrives, gradually</a></h2>
<p>Designers and developers were pushing the bounds of what browsers were capable of. Browsers were handling it all somewhat poorly. All the fixes and workarounds and libraries were arcane, brittle, error-prone, and/or heavy.</p>
<p>Clearly, browsers needed some new functionality. But just slopping something in wouldn’t help; Microsoft had done plenty of that, and it had mostly made a mess.</p>
<p>Several struggling attempts began. With the <span class="caps">W3C</span>’s head still squarely up its own ass — even explicitly rejecting proposed enhancements to <span class="caps">HTML</span>, in favor of snorting <span class="caps">XML</span> — some folks from (active) browser vendors Apple, Mozilla, and Opera decided to make their own clubhouse. <span class="caps">WHATWG</span> came into existence in June 2004, and they began work on <span class="caps">HTML5</span>. (It would end up defining error-handling very explicitly, which completely obviated the need for <span class="caps">XHTML</span> and eliminated a number of security concerns when working with arbitrary <span class="caps">HTML</span>. Also it gave us some new goodies, like native audio, video, and form controls for dates and colors and other stuff that had been clumsily handled by JavaScript-powered custom controls. And, um, still often are.)</p>
<p>Then there was <span class="caps">CSS</span> 3. I’m not sure when it started to exist. It emerged slowly, struggling, like a chick hatching from an egg and taking its damn sweet fucking time to actually get implemented anywhere.</p>
<p>I’m having to do a lot of educated guessing here, but I <em>think</em> it began with <code>border-radius</code>. Specifically, with <code>-moz-border-radius</code>. I don’t know when it was first introduced, but the Mozilla bug tracker has mentions of it as far back as 1999.</p>
<p>See, Firefox’s own <span class="caps">UI</span> is rendered <em>with <span class="caps">CSS</span></em>. If Mozilla wanted to do something that couldn’t be done with <span class="caps">CSS</span>, they added a property of their own, prefixed with <code>-moz-</code> to indicate it was their own invention. And when there’s no real harm in doing so, they leave the property accessible to websites as well.</p>
<p>My guess, then, is that the push for <span class="caps">CSS</span> 3 really began when Firefox took off and designers discovered <code>-moz-border-radius</code>. Suddenly, built-in rounded corners were available! No more fucking around in Photoshop; you only needed to write a single line! Practically overnight, everything everywhere had its corners filed down.</p>
<p>And from there, things snowballed. Common problems were addressed one at a time by new <span class="caps">CSS</span> features, which were clustered together into a new <span class="caps">CSS</span> version: <span class="caps">CSS</span> 3. The big ones were solutions to the design problems mentioned before:</p>
<ul>
<li>Rounded corners, provided by <code>border-radius</code>.</li>
<li>Gradients, provided by <code>linear-gradient()</code> and friends.</li>
<li>Multiple backgrounds, which weren’t exactly a pressing concern, but which turned out to make some other stuff easier.</li>
<li>Translucency, provided by <code>opacity</code> and colors with an alpha channel.</li>
<li>Box shadows.</li>
<li>Text shadows, which had been in <span class="caps">CSS</span> 2 but dropped in 2.1 and never implemented anyway.</li>
<li>Border images, so you could do even fancier things than mere rounded borders.</li>
<li>Transitions and animations, now doable with ease without needing jQuery (or any <span class="caps">JS</span> at all).</li>
<li><code>:nth-child()</code>, which solved the alternating rows problem with pure <span class="caps">CSS</span>.</li>
<li>Transformations. Wait, what? This kinda leaked in from <span class="caps">SVG</span>, which browsers were also being expected to implement, and which is built heavily around transforms. The code was already there, so, hey, now we can rotate stuff with <span class="caps">CSS</span>! Couldn’t do <em>that</em> before. Cool.</li>
<li>Web fonts, which had been in <span class="caps">CSS</span> for some time but only ever implemented in <span class="caps">IE</span> and only with some goofy <span class="caps">DRM</span>-laden font format. Now we weren’t limited to the four bad fonts that ship with Windows and that no one else has!</li>
</ul>
<p>These were pretty great! They didn’t solve any layout problems, but they <em>did</em> address aesthetic issues that designers had been clumsily working around by using loads of images and/or JavaScript. That meant less stuff to download and more text used instead of images, both of which were pretty good for the Web.</p>
<p>The grand irony is that all the stuff you could do with these features went out of style almost immediately, and now we’re back to flat rectangles again.</p>
<h3 id="browser-prefixing-hell"><a class="toclink" href="proxy.php?url=#browser-prefixing-hell">Browser prefixing hell</a></h3>
<p>Alas! All was still not right with the world.</p>
<p>Several of these new gizmos were, I believe, initially developed by browser vendors and prefixed. Some later ones were designed by the <span class="caps">CSS</span> committee but implemented by browsers while the design was still in flux, and thus also prefixed.</p>
<p>So began <em>prefix hell</em>, which continues to this day.</p>
<p>Mozilla had <code>-moz-border-radius</code>, so when Safari implemented it, it was named <code>-webkit-border-radius</code> (“WebKit” being the name of Apple’s <span class="caps">KHTML</span> fork). Then the <span class="caps">CSS</span> 3 spec standardized it and called it just <code>border-radius</code>. That meant that if you wanted to use rounded borders, you actually needed to give <em>three</em> rules:</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="nt">element</span><span class="w"> </span><span class="p">{</span>
<span class="linenos">2</span><span class="w"> </span><span class="kp">-moz-</span><span class="k">border-radius</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="kt">em</span><span class="p">;</span>
<span class="linenos">3</span><span class="w"> </span><span class="kp">-webkit-</span><span class="k">border-radius</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="kt">em</span><span class="p">;</span>
<span class="linenos">4</span><span class="w"> </span><span class="k">border-radius</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="kt">em</span><span class="p">;</span>
<span class="linenos">5</span><span class="p">}</span>
</code></pre></div>
<p>The first two made the effect actually work in current browsers, and the last one was future-proofing: when browsers implemented the real rule and dropped the prefixed ones, it would take over.</p>
<p>You had to do this <em>every fucking time</em>, since <span class="caps">CSS</span> isn’t a programming language and has no macros or functions or the like. Sometimes Opera and <span class="caps">IE</span> would have their own implementations with <code>-o-</code> and <code>-ms-</code> prefixes, bringing the total to five copies. It got much worse with gradients; the syntax went through a number of major incompatible revisions, so you couldn’t even rely on copy/pasting and changing the property name!</p>
<p>And plenty of folks, well, fucked it up. I can’t blame them too much; I mean, this sucks. But enough pages used <em>only</em> the prefixed forms, and not the final form, that browsers had to keep supporting the prefixed form for longer than they would’ve liked to avoid breaking stuff. And if the prefixed form still works and it’s what you’re used to writing, then maybe you still won’t bother with the unprefixed one.</p>
<p>Worse, <em>some</em> people would <em>only</em> use the form that worked in their pet choice of browser. This got especially bad with the rise of mobile web browsers. The built-in browsers on iOS and Android are Safari (WebKit) and Chrome (originally WebKit, now a fork), so you only “needed” to use the <code>-webkit-</code> properties. Which made things difficult for Mozilla when it released <a href="proxy.php?url=https://www.mozilla.org/en-US/firefox/mobile/">Firefox for Android</a>.</p>
<p>Hey, remember that whole debacle with <span class="caps">IE</span> 6? Here we are again! It was bad enough that Mozilla eventually decided to <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/WebKit_Extensions#Supported_in_Firefox_with_-webkit-_prefix">implement</a> a number of <code>-webkit-</code> properties, which remain supported even in desktop Firefox to this day. The situation is goofy enough that Firefox now supports some effects <em>only</em> via these properties, like <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-text-stroke"><code>-webkit-text-stroke</code></a>, which isn’t being standardized.</p>
<p>Even better, Chrome’s current forked engine is called Blink, so <em>technically</em> it shouldn’t be using <code>-webkit-</code> properties either. And yet, here we are. At least it’s not as bad as the <a href="proxy.php?url=https://webaim.org/blog/user-agent-string-history/">user agent string mess</a>.</p>
<p>Browser vendors have pretty much abandoned prefixing, now; instead they hide experimental features behind flags (so they’ll only work on the developer’s machine), and new features are theoretically designed to be smaller and easier to stabilize.</p>
<p>This mess was probably a huge motivating factor for the development of <a href="proxy.php?url=https://sass-lang.com/">Sass</a> and <a href="proxy.php?url=http://lesscss.org/"><span class="caps">LESS</span></a>, two languages that produce <span class="caps">CSS</span>. Or… two <span class="caps">CSS</span> preprocessors, maybe. They have very similar goals: both add variables, functions, and some form of macros to <span class="caps">CSS</span>, allowing you to eliminate a lot of the repetition and browser hacks and other nonsense from your stylesheets. Hell, this blog <a href="proxy.php?url=https://github.com/eevee/eev.ee/tree/988fc2b4547ee41388f29c4bad622c492c4c6f77/theme/static/sass">still uses <span class="caps">SCSS</span></a>, though its use has gradually decreased over time.</p>
<h3 id="flexbox"><a class="toclink" href="proxy.php?url=#flexbox">Flexbox</a></h3>
<p>But then, like an angel descending from heaven… <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flexible_Box_Layout">flexbox</a>.</p>
<p>Flexbox has been around for a <em>long</em> time — <a href="proxy.php?url=https://www.caniuse.com/#feat=flexbox">allegedly</a> it had partial support in Firefox 2, back in 2006! It went through several incompatible revisions and took ages to stabilize. Then <span class="caps">IE</span> took ages to implement it, and you don’t really want to rely on layout tools that only work for half your audience. It’s only relatively recently (2015? Later?) that flexbox has had sufficiently broad support to use safely. And I could swear I still run into folks whose current Safari doesn’t recognize it at all without prefixing, even though Safari supposedly dropped the prefixes five years ago…</p>
<p>Anyway, flexbox is a <span class="caps">CSS</span> implementation of a pretty common <span class="caps">GUI</span> layout tool: you have a parent with some children, and the parent has some amount of space available, and it gets divided automatically between the children. You know, it <em>puts things next to each other</em>.</p>
<p>The general idea is that the browser computes how much space the parent has available and the “initial size” of each child, figures out how much extra space there is, and distributes it according to the flexibleness of each child. Think of a toolbar: you might want each button to have a fixed size (a flex of 0), but want to add spacers that share any leftover space equally, so you’d give them a flex of 1.</p>
<p>Once that’s done, you have a number of quality-of-life options at your disposal, too: you can distribute the extra space <em>between</em> the children instead, you can tell the children to stretch to the same height or align them in various ways, and you can even have them wrap into multiple rows if they won’t all fit!</p>
<p>With this, we can take yet another crack at that <a href="proxy.php?url=https://eev.ee/media/2020-02-css/thumbnail-grids.html#flexbox">thumbnail grid</a>:</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="p">.</span><span class="nc">thumbnail-grid</span><span class="w"> </span><span class="p">{</span>
<span class="linenos">2</span><span class="w"> </span><span class="k">display</span><span class="p">:</span><span class="w"> </span><span class="kc">flex</span><span class="p">;</span>
<span class="linenos">3</span><span class="w"> </span><span class="k">flex-wrap</span><span class="p">:</span><span class="w"> </span><span class="kc">wrap</span><span class="p">;</span>
<span class="linenos">4</span><span class="p">}</span>
<span class="linenos">5</span><span class="p">.</span><span class="nc">thumbnail-grid</span><span class="w"> </span><span class="nt">li</span><span class="w"> </span><span class="p">{</span>
<span class="linenos">6</span><span class="w"> </span><span class="k">flex</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="mi">250</span><span class="kt">px</span><span class="p">;</span>
<span class="linenos">7</span><span class="p">}</span>
</code></pre></div>
<p>This is miraculous. I forgot all about <code>inline-block</code> overnight and mostly salivated over this until it was universally supported. It even expresses very clearly what I want.</p>
<p>…almost. It still has the problem that too-wide cells will break the grid, since it’s <em>still</em> a horizontal row wrapped onto several independent lines. It’s pretty damn cool, though, and solves a number of other layout problems. Surely this is good enough. Unless…?</p>
<p>I’d say mass adoption of flexbox marked the beginning of the modern era of <span class="caps">CSS</span>. But there was one lingering problem…</p>
<h3 id="the-slow-agonizing-death-of-ie"><a class="toclink" href="proxy.php?url=#the-slow-agonizing-death-of-ie">The slow, agonizing death of IE</a></h3>
<p><span class="caps">IE</span> 6 took a long, long, <em>long</em> time to go away. It didn’t drop below 10% market share (still a huge chunk) until early 2010 or so.</p>
<p>Firefox hit 1.0 at the end of 2004. <span class="caps">IE</span> 7 wasn’t released until two years later, it offered only modest improvements, it suffered from compatibility problems with stuff built for <span class="caps">IE</span> 6, and the <span class="caps">IE</span> 6 holdouts (many of whom were not Computer People) generally saw no reason to upgrade. Vista shipped with <span class="caps">IE</span> 7, but Vista was kind of a flop — I don’t believe it ever came close to overtaking <span class="caps">XP</span>, not in its entire lifetime.</p>
<p>Other factors included corporate <span class="caps">IT</span> policies, which often take the form of “never upgrade anything ever” — and often for good reason, as I heard endless tales of internal apps that only worked in <span class="caps">IE</span> 6 for all manner of horrifying reasons. Then there was the <em>entirety of South Korea</em>, which was <em>legally required</em> to use <span class="caps">IE</span> 6 because they’d enshrined in law some <a href="proxy.php?url=https://www.washingtonpost.com/world/asia_pacific/due-to-security-law-south-korea-is-stuck-with-internet-explorer-for-online-shopping/2013/11/03/ffd2528a-3eff-11e3-b028-de922d7a3f47_story.html">security requirements</a> that could only be implemented with an <span class="caps">IE</span> 6 ActiveX control.</p>
<p>So if you maintained a website that was used — or worse, <em>required</em> — by people who worked for businesses or lived in other countries, you were pretty much stuck supporting <span class="caps">IE</span> 6. Folks making little personal tools and websites abandoned <span class="caps">IE</span> 6 compatibility early on and plastered their sites with increasingly obnoxious banners taunting anyone who dared show up using it… but if you were someone’s boss, why would you tell them it’s okay to drop 20% of your potential audience? Just work harder!</p>
<p>The tension grew over the years, as <span class="caps">CSS</span> became more capable and <span class="caps">IE</span> 6 remained an anchor. It still didn’t even understand <em><span class="caps">PNG</span> alpha</em> without workarounds, and meanwhile we were starting to get more critical features like native video in <span class="caps">HTML5</span>. The workarounds grew messier, and the list of features you basically just couldn’t use grew longer. (I’d show you what my blog looks like in <span class="caps">IE</span> 6, but I don’t think it can even connect — the <span class="caps">TLS</span> stuff it supports is so ancient and broken that it’s been disabled on most servers!)</p>
<p>Shoutouts, by the way, to some folks on the YouTube team, who in July 2009 <a href="proxy.php?url=https://www.theverge.com/2019/5/4/18529381/google-youtube-internet-explorer-6-kill-plot-engineer">added a warning banner</a> imploring <span class="caps">IE</span> 6 users to switch to <em>anything</em> else — without asking anyone for approval. “Within one month… over 10 percent of global <span class="caps">IE6</span> traffic had dropped off.” Not all heroes wear capes.</p>
<p>I’d mark the beginning of the end as the day YouTube <em>actually</em> dropped <span class="caps">IE</span> 6 support — March 13, 2010, almost nine years after its release. I don’t know how much of a <em>direct</em> impact YouTube has on corporate users or the South Korean government, but a massive web company dropping an entire browser sends a pretty strong message.</p>
<p>There were other versions of <span class="caps">IE</span>, of course, and many of them were messy headaches in their own right. But each subsequent one became less of a pain, and nowadays you don’t even have to think too much about testing in <span class="caps">IE</span> (now Edge). Just in time for Microsoft to scrap their own rendering engine and turn their browser into a Chrome clone.</p>
<h2 id="now"><a class="toclink" href="proxy.php?url=#now">Now</a></h2>
<p><span class="caps">CSS</span> is pretty great now. You don’t need weird fucking hacks just to put things next to each other. Browser dev tools are built in, now, and are fucking amazing — Firefox has started specifically warning you when some <span class="caps">CSS</span> properties won’t take effect because of the values of others! Obscure implicit side effects like “stacking contexts” (whatever those are) can now be set explicitly, with properties like <code>isolation: isolate</code>.</p>
<p>In fact, let me just list everything that I can think of that you can do in <span class="caps">CSS</span> now. This isn’t a guide to all possible uses of styling, but if your <span class="caps">CSS</span> knowledge hasn’t been updated since 2008, I hope this whets your appetite. And this stuff is just <span class="caps">CSS</span>! So many things that used to be impossible or painful or require clumsy plugins are now natively supported — audio, video, custom drawing, 3D rendering… not to mention the vast ergonomic improvements to JavaScript.</p>
<h3 id="layout"><a class="toclink" href="proxy.php?url=#layout">Layout</a></h3>
<p>A <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout">grid</a> container can do pretty much anything tables can do, and more, including automatically determining how many columns will fit. It’s fucking amazing. More on that below.</p>
<p>A <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flexible_Box_Layout">flexbox</a> container lays out its children in a row or column, allowing each child to declare its “default” size and what proportion of leftover space it wants to consume. Flexboxes can wrap, rearrange children without changing source order, and align children in a number of ways.</p>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Columns">Columns</a> will pour text into, well, multiple columns.</p>
<p>The <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/box-sizing"><code>box-sizing</code></a> property lets you opt into the <span class="caps">IE</span> box model on a per-element basis, for when you need an entire element to take up a fixed amount of space and need padding/borders to <em>subtract</em> from that.</p>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/display"><code>display: contents</code></a> dumps an element’s contents out into its parent, as if it weren’t there at all. <code>display: flow-root</code> is basically an automatic clearfix, only a decade too late.</p>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/width"><code>width</code></a> can now be set to <code>min-content</code>, <code>max-content</code>, or the <code>fit-content()</code> function for more flexible behavior.</p>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/white-space"><code>white-space: pre-wrap</code></a> preserves whitespace, but breaks lines where necessary to avoid overflow. Also useful is <code>pre-line</code>, which collapses sequences of spaces down to a single space, but preserves literal newlines.</p>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/text-overflow"><code>text-overflow</code></a> cuts off overflowing text with an ellipsis (or custom character) when it would overflow, rather than simply truncating it. Also specced is the ability to fade out the text, but this is as yet unimplemented.</p>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/shape-outside"><code>shape-outside</code></a> alters the shape used when wrapping text around a float. It can even use the alpha channel of an image as the shape.</p>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/resize"><code>resize</code></a> gives an arbitrary element a resize handle (as long as it has <code>overflow</code>).</p>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/writing-mode"><code>writing-mode</code></a> sets the direction that text flows. If your design needs to work for multiple writing modes, a number of <span class="caps">CSS</span> properties that mention left/right/top/bottom have alternatives that describe directions in terms of the writing mode: <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/inset-block"><code>inset-block</code></a> and <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/inset-inline"><code>inset-inline</code></a> for position, <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/block-size"><code>block-size</code></a> and <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/inline-size"><code>inline-size</code></a> for width/height, <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/border-block"><code>border-block</code></a> and <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/border-inline"><code>border-inline</code></a> for borders, and similar for padding and margins.</p>
<h3 id="aesthetics"><a class="toclink" href="proxy.php?url=#aesthetics">Aesthetics</a></h3>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Transitions">Transitions</a> smoothly interpolate a value whenever it changes, whether due to an effect like <code>:hover</code> or e.g. a class being added from JavaScript. <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Animations">Animations</a> are similar, but play a predefined animation automatically. Both can use a number of different <a href="proxy.php?url=https://easings.net/en">easing functions</a>.</p>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/border-radius"><code>border-radius</code></a> rounds off the corners of a box. The corners can all be different sizes, and can be circular or elliptical. The curve also applies to the border, background, and any box shadows.</p>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow">Box shadows</a> can be used for the obvious effect of casting a drop shadow. You can also use multiple shadows and <code>inset</code> shadows for a variety of clever effects.</p>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/text-shadow"><code>text-shadow</code></a> does what it says on the tin, though you can also stack several of them for a rough approximation of a text outline.</p>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/transform"><code>transform</code></a> lets you apply an arbitrary matrix transformation to an element — that is, you can scale, rotate, skew, translate, and/or do perspective transform, all without affecting layout.</p>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/filter"><code>filter</code></a> (distinct from the <span class="caps">IE</span> 6 one) offers a handful of specific visual filters you can apply to an element. Most of them affect color, but there’s also a <code>blur()</code> and a <code>drop-shadow()</code> (which, unlike <code>box-shadow</code>, applies to an element’s appearance rather than its containing box).</p>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/linear-gradient"><code>linear-gradient()</code></a>, <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/radial-gradient"><code>radial-gradient()</code></a>, the new and less-supported <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/conic-gradient"><code>conic-gradient()</code></a>, and their <code>repeating-*</code> variants all produce gradient images and can be used anywhere in <span class="caps">CSS</span> that an image is expected, most commonly as a <code>background-image</code>.</p>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color"><code>scrollbar-color</code></a> changes the scrollbar color, with the downside of reducing the scrollbar to a very simple thumb-and-track in current browsers.</p>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/background-size"><code>background-size: cover</code> and <code>contain</code></a> will scale a background image proportionally, either big enough to completely cover the element (even if cropped) or small enough to exactly fit inside it (even if it doesn’t cover the entire background).</p>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit"><code>object-fit</code></a> is a similar idea but for non-background media, like <code><img></code>s. The related <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/object-position"><code>object-position</code></a> is like <code>background-position</code>.</p>
<p>Multiple backgrounds are possible, which is especially useful with gradients — you can stack multiple gradients, other background images, and a solid color on the bottom.</p>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/text-decoration"><code>text-decoration</code></a> is fancier than it used to be; you can now set the color of the line and use several different kinds of lines, including dashed, dotted, and wavy.</p>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Lists_and_Counters/Using_CSS_counters"><span class="caps">CSS</span> counters</a> can be used to number arbitrary elements in an arbitrary way, exposing the counting ability of <code><ol></code> to any set of elements you want. </p>
<p>The <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/::marker"><code>::marker</code></a> pseudo-element allows you to style a list item’s marker box, or even replace it outright with a custom counter. Browser support is spotty, but improving. Similarly, the <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/@counter-style"><code>@counter-style</code></a> at-rule implements an entirely new counter style (like 1 2 3, i ii iii, A B C, etc.) which you can then use anywhere, though only Firefox supports it so far.</p>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/image-set"><code>image-set()</code></a> provides a list of candidate images and lets the browser choose the most appropriate one based on the pixel density of the user’s screen.</p>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face"><code>@font-face</code></a> defines a font that can be downloaded, though you can avoid figuring out how to use it correctly by using <a href="proxy.php?url=https://developers.google.com/fonts/">Google Fonts</a>.</p>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events"><code>pointer-events: none</code></a> makes an element ignore the mouse entirely; it can’t be hovered, and clicks will go straight through it to the element below.</p>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering"><code>image-rendering</code></a> can force an image to be resized nearest-neighbor rather than interpolated, though browser support is still spotty and you may need to also include some vendor-specific properties.</p>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/clip-path"><code>clip-path</code></a> crops an element to an arbitrary shape. There’s also <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/mask"><code>mask</code></a> for arbitrary alpha masking, but browser support is spotty and hoo boy is this one complicated.</p>
<h3 id="syntax-and-misc"><a class="toclink" href="proxy.php?url=#syntax-and-misc">Syntax and misc</a></h3>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/@supports"><code>@supports</code></a> lets you explicitly write different <span class="caps">CSS</span> depending on what the browser supports, though it’s nowhere near as useful nowadays as it would’ve been in 2004.</p>
<p><code>A > B</code> selects immediate children. <code>A ~ B</code> selects siblings. <code>A + B</code> selects immediate (element) siblings. Square brackets can do a <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors">bunch of stuff</a> to select based on attributes; most obvious is <code>input[type=checkbox]</code>, though you can also do interesting things with matching parts of <code><a href></code>.</p>
<p>There are a whole bunch of pseudo-classes now. Many of them are for form elements: <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/:enabled"><code>:enabled</code></a> and <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/:disabled"><code>:disabled</code></a>; <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/:checked"><code>:checked</code></a> and <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/:indeterminate"><code>:indeterminate</code></a> (also apply to radio and <code><option></code>); <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/:required"><code>:required</code></a> and <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/:optional"><code>:optional</code></a>; <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/:read-write"><code>:read-write</code></a> and <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/:read-only"><code>:read-only</code></a>; <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/:in-range"><code>:in-range</code></a>/<a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/:out-of-range"><code>:out-of-range</code></a> and <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/:valid"><code>:valid</code></a>/<a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/:invalid"><code>:invalid</code></a> (for use with <span class="caps">HTML5</span> client-side form validation); <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/:focus"><code>:focus</code></a> and <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-within"><code>:focus-within</code></a>; and <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/:default"><code>:default</code></a> (which selects the default form button and any pre-selected checkboxes, radio buttons, and <code><option></code>s).</p>
<p>For targeting specific elements within a set of siblings, we have: <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/:first-child"><code>:first-child</code></a>, <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/:last-child"><code>:last-child</code></a>, and <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/:only-child"><code>:only-child</code></a>; <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/:first-of-type"><code>:first-of-type</code></a>, <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/:last-of-type"><code>:last-of-type</code></a>, and <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/:only-of-type"><code>:only-of-type</code></a> (where “type” means tag name); and <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-child"><code>:nth-child()</code></a>, <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-last-child"><code>:nth-last-child()</code></a>, <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-of-type"><code>:nth-of-type()</code></a>, and <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-last-of-type"><code>:nth-last-of-type()</code></a> (to select every second, third, etc. element).</p>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/:not"><code>:not()</code></a> inverts a selector. <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/:empty"><code>:empty</code></a> selects elements with no children and no text. <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/:target"><code>:target</code></a> selects the element jumped to with a <span class="caps">URL</span> fragment (e.g. if the address bar shows <code>index.html#foo</code>, this selects the element whose <span class="caps">ID</span> is <code>foo</code>).</p>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/::before"><code>::before</code></a> and <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/::after"><code>::after</code></a> should have two colons now, to indicate that they create pseudo-elements rather than merely scoping the selector they’re attached to. <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/::selection"><code>::selection</code></a> customizes how selected text appears; <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/::placeholder"><code>::placeholder</code></a> customizes how placeholder text (in text fields) appears.</p>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/@media">Media queries</a> do just a whole bunch of stuff so your page can adapt based on how it’s being viewed. The <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme"><code>prefers-color-scheme</code></a> media query tells you if the user’s system is set to a light or dark theme, so you can adjust accordingly without having to ask.</p>
<p>You can write translucent <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/color_value">colors</a> as <code>#rrggbbaa</code> or <code>#rgba</code>, as well as using the <code>rgba()</code> and <code>hsla()</code> functions.</p>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/angle">Angles</a> can be described as fractions of a full circle with the <code>turn</code> unit. Of course, <code>deg</code> and <code>rad</code> (and <code>grad</code>) are also available.</p>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties"><span class="caps">CSS</span> variables</a> (officially, “custom properties”) let you specify arbitrary named values that can be used anywhere a value would appear. You can use this to reduce the amount of <span class="caps">CSS</span> fiddling needs doing in JavaScript (e.g., recolor a complex part of a page by setting a <span class="caps">CSS</span> variable instead of manually adjusting a number of properties), or have a generic component that reacts to variables set by an ancestor.</p>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/calc"><code>calc()</code></a> computes an arbitrary expression and updates automatically (though it’s somewhat obviated by <code>box-sizing</code>).</p>
<p>The <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/length"><code>vw</code>, <code>vh</code>, <code>vmin</code>, and <code>vmax</code> units</a> let you specify lengths as a fraction of the viewport’s width or height, or whichever of the two is bigger/smaller.</p>
<hr />
<p>Phew! I’m sure I’m forgetting plenty and folks will have even longer lists of interesting tidbits in the comments. Thanks for saving me some effort! Now I can stop browsing <span class="caps">MDN</span> and do this final fun part.</p>
<h3 id="state-of-the-art-thumbnail-grid"><a class="toclink" href="proxy.php?url=#state-of-the-art-thumbnail-grid">State of the art thumbnail grid</a></h3>
<p>At long last, we arrive at the final and objectively correct way to construct a thumbnail grid: using <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout"><span class="caps">CSS</span> grid</a>. You can tell this is the right thing to use because it has “grid” in the name. Modern <span class="caps">CSS</span> features are pretty great about letting you say the thing you want and having it happen, rather than trying to coax it into happening implicitly via voodoo.</p>
<p>And it is oh so simple:</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="p">.</span><span class="nc">thumbnail-grid</span><span class="w"> </span><span class="p">{</span>
<span class="linenos">2</span><span class="w"> </span><span class="k">display</span><span class="p">:</span><span class="w"> </span><span class="k">grid</span><span class="p">;</span>
<span class="linenos">3</span><span class="w"> </span><span class="k">grid</span><span class="p">:</span><span class="w"> </span><span class="kc">auto</span><span class="o">-</span><span class="k">flow</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="nf">repeat</span><span class="p">(</span><span class="kc">auto</span><span class="nv">-fit</span><span class="p">,</span><span class="w"> </span><span class="nf">minmax</span><span class="p">(</span><span class="mi">250</span><span class="kt">px</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="n">fr</span><span class="p">));</span>
<span class="linenos">4</span><span class="p">}</span>
</code></pre></div>
<p>Done! That <a href="proxy.php?url=https://eev.ee/media/2020-02-css/thumbnail-grids.html#grid">gives you a grid</a>. You have myriad other twiddles to play with, just as with flexbox, but that’s the basic idea. You don’t even need to style the elements themselves; most of the layout work is done in the container.</p>
<p>The <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/grid"><code>grid</code> shorthand property</a> looks a little intimidating, but only because it’s so flexible. It’s saying: fill the grid one row at a time, generating as many rows as necessary; make as many 250px columns as will fit, and share any leftover space between them equally.</p>
<p><span class="caps">CSS</span> grids are also handy for laying out <code><dl></code>s, something that’s historically been a massive pain to make work — a <code><dl></code> contains any number of <code><dt></code>s followed by any number of <code><dd></code>s (including zero), and the only way to style this until grid was to float the <code><dt></code>s, which meant they had to have a fixed width. Now you can just tell the <code><dt></code>s to go in the first column and <code><dd></code>s to go in the second, and grid will take care of the rest.</p>
<p>And laying out your page? That whole sidebar thing? Check out how easy that is:</p>
<div class="highlight"><pre><span></span><code><span class="linenos"> 1</span><span class="nt">body</span><span class="w"> </span><span class="p">{</span>
<span class="linenos"> 2</span><span class="w"> </span><span class="k">display</span><span class="p">:</span><span class="w"> </span><span class="k">grid</span><span class="p">;</span>
<span class="linenos"> 3</span><span class="w"> </span><span class="k">grid-template</span><span class="p">:</span>
<span class="linenos"> 4</span><span class="w"> </span><span class="s2">"header header header"</span>
<span class="linenos"> 5</span><span class="w"> </span><span class="s2">"left-sidebar main-content right-sidebar"</span>
<span class="linenos"> 6</span><span class="w"> </span><span class="s2">"footer footer footer"</span>
<span class="linenos"> 7</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="mi">1</span><span class="n">fr</span><span class="w"> </span><span class="mi">6</span><span class="n">fr</span><span class="w"> </span><span class="mi">1</span><span class="n">fr</span>
<span class="linenos"> 8</span><span class="w"> </span><span class="p">;</span>
<span class="linenos"> 9</span><span class="p">}</span>
<span class="linenos">10</span><span class="nt">body</span><span class="w"> </span><span class="o">></span><span class="w"> </span><span class="nt">header</span><span class="w"> </span><span class="p">{</span>
<span class="linenos">11</span><span class="w"> </span><span class="k">grid-area</span><span class="p">:</span><span class="w"> </span><span class="n">header</span><span class="p">;</span>
<span class="linenos">12</span><span class="p">}</span>
<span class="linenos">13</span><span class="p">#</span><span class="nn">left-sidebar</span><span class="w"> </span><span class="p">{</span>
<span class="linenos">14</span><span class="w"> </span><span class="k">grid-area</span><span class="p">:</span><span class="w"> </span><span class="kc">left</span><span class="o">-</span><span class="n">sidebar</span><span class="p">;</span>
<span class="linenos">15</span><span class="p">}</span>
<span class="linenos">16</span><span class="c">/* ... etc ... */</span>
</code></pre></div>
<p>Done. Easy. It doesn’t matter what order the parts appear in the markup, either.</p>
<h3 id="on-the-other-hand"><a class="toclink" href="proxy.php?url=#on-the-other-hand">On the other hand</a></h3>
<p>The web is still a <em>little bit</em> of a disaster. A lot of folks don’t even know that flexbox and grid are supported <a href="proxy.php?url=https://www.caniuse.com/#feat=css-grid">almost universally</a> now; but given how long it took to get from early spec work to broad implementation, I can’t really blame them. I saw a brand new little site just yesterday that consisted mostly of a huge list of “thumbnails” of various widths, and it used floats! Not even <code>inline-block</code>! I don’t know how we managed to teach everyone about all the hacks required to make that work, but somehow haven’t gotten the word out about flexbox.</p>
<p>But far worse than that: I still regularly encounter sites that do their entire page layout with <em>JavaScript</em>. If you use <a href="proxy.php?url=https://addons.mozilla.org/en-US/firefox/addon/umatrix/">uMatrix</a>, your first experience is with a pile of text overlapping a pile of other text. Surely this is a step backwards? What are you possibly doing that your header and sidebar can only be laid out correctly by executing code? It’s not like the page loads with <em>no</em> <span class="caps">CSS</span> — nothing in plain <span class="caps">HTML</span> will overlap by default! You have to tell it to do that!</p>
<p>And then there’s the mobile web, which despite everyone’s good intentions, has kind of turned out to be a failure. The idea was that you could use <span class="caps">CSS</span> media queries to fit your normal site on a phone screen, but instead, most major sites have entirely separate mobile versions. Which means that either the mobile site is missing a bunch of important features and I’ll have to awkwardly navigate that on my phone anyway, or the desktop site is full of crap that nobody actually needs.</p>
<p>(Meanwhile, Google’s own Android versions of Docs/Sheets/etc. have, like, 5% of the features of the Web versions? Not sure what to make of that.)</p>
<p>Hmm. Strongly considering writing something that goes more into detail about improvements to <span class="caps">CSS</span> since the Firefox 3 era, similar to <a href="proxy.php?url=https://eev.ee/blog/2017/10/07/javascript-got-better-while-i-wasnt-looking/">the one I wrote for JavaScript</a>. But this post is long enough.</p>
<h2 id="some-futures-that-never-were"><a class="toclink" href="proxy.php?url=#some-futures-that-never-were">Some futures that never were</a></h2>
<p>I don’t know what’s coming next in <span class="caps">CSS</span>, especially now that flexbox and grid have solved all our problems. I’m vaguely aware of some work being done on more extensive math support, and possibly some functions for altering colors like in Sass. There’s a <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/API/CSS_Painting_API">painting <span class="caps">API</span></a> that lets you generate backgrounds on the fly with JavaScript using the canvas <span class="caps">API</span>, which is… quite something. Apparently it’s now in spec that you can use <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/attr"><code>attr()</code></a> (which evaluates to the value of an <span class="caps">HTML</span> attribute) as the value for any property, which seems cool and might even let you implement <span class="caps">HTML</span> tables entirely in <span class="caps">CSS</span>, but you could do the same thing with variables. I mean, um, custom properties. I’m more excited about <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/:is"><code>:is()</code></a>, which matches any of a list of selectors, and <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout/Subgrid">subgrid</a>, which lets you add some nesting to a grid but keep grandchildren still aligned to it.</p>
<p>Much easier is to list some things that <em>were</em> the future, but fizzled out.</p>
<ul>
<li>
<p><a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/display"><code>display: run-in</code></a> has been part of <span class="caps">CSS</span> since version 2 (way back in ‘98), but it’s basically unsupported. The idea is that a “run-in” box is inserted, inline, into the next block, so this:</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="p"><</span><span class="nt">h2</span> <span class="na">style</span><span class="o">=</span><span class="s">"display: run-in;"</span><span class="p">></span>Title<span class="p"></</span><span class="nt">h2</span><span class="p">></span>
<span class="linenos">2</span><span class="p"><</span><span class="nt">p</span><span class="p">></span>Paragraph<span class="p"></</span><span class="nt">p</span><span class="p">></span>
<span class="linenos">3</span><span class="p"><</span><span class="nt">p</span><span class="p">></span>Paragraph<span class="p"></</span><span class="nt">p</span><span class="p">></span>
</code></pre></div>
<p>displays like this:</p>
<blockquote>
<p><strong>Title</strong> Paragraph</p>
<p>Paragraph</p>
</blockquote>
<p>And, ah, hm, I’m starting to see why it’s unsupported. It <em>used</em> to exist in WebKit, but was apparently so unworkable as to be removed six years ago.</p>
</li>
<li>
<p><span class="dquo">“</span>Alternate stylesheets” were popular in the early 00s, at least on a few of my friends’ websites. The idea was that you could list <em>more than one</em> stylesheet for your site (presumably for different themes), and the browser would give the user a list of them. Alas, that list was always squirrelled away in a menu with no obvious indication of when it was actually populated, so in the end, everyone who wanted multiple themes just implemented an in-page theme switcher themselves.</p>
<p>This feature is <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/Alternative_style_sheets">still supported</a>, but apparently Chrome never bothered implementing it, so it’s effectively dead.</p>
</li>
<li>
<p>More generally, the original <span class="caps">CSS</span> spec clearly expects users to be able to write their own <span class="caps">CSS</span> for a website — right in paragraph 2 it says</p>
<blockquote>
<p>…the reader may have a personal style sheet to adjust for human or technological handicaps.</p>
</blockquote>
<p>Hey, that sounds cool. But it never materialized as a browser feature. Firefox has <a href="proxy.php?url=http://kb.mozillazine.org/UserContent.css"><code>userContent.css</code></a> and some <span class="caps">URL</span> selectors for writing per-site rules, but that’s relatively obscure.</p>
<p>Still, there’s clearly demand for the concept, as evidenced by the popularity of the Stylish extension — which does just this. (Too bad it was <a href="proxy.php?url=https://robertheaton.com/2018/07/02/stylish-browser-extension-steals-your-internet-history/">bought by some chucklefucks who started using it to suck up browser data to sell to advertisers</a>. Use <a href="proxy.php?url=https://addons.mozilla.org/en-US/firefox/addon/styl-us/">Stylus</a> instead.)</p>
</li>
<li>
<p>A common problem (well, for me) is that of styling the <em>label</em> for a checkbox, depending on its state. Styling the checkbox itself is easy enough with the <code>:checked</code> pseudo-selector. But if you arrange a checkbox and its label in the obvious way:</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="p"><</span><span class="nt">label</span><span class="p">><</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">"checkbox"</span><span class="p">></span> Description of what this does<span class="p"></</span><span class="nt">label</span><span class="p">></span>
</code></pre></div>
<p>…then <span class="caps">CSS</span> has no way to target either the <code><label></code> element or the text node. jQuery’s (originally custom) selector engine offered a custom <code>:has()</code> pseudo-class, which could be used to express this:</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="c">/* checkbox label turns bold when checked */</span>
<span class="linenos">2</span><span class="nt">label</span><span class="p">:</span><span class="nd">has</span><span class="o">(</span><span class="nt">input</span><span class="p">:</span><span class="nd">checked</span><span class="o">)</span><span class="w"> </span><span class="p">{</span>
<span class="linenos">3</span><span class="w"> </span><span class="k">font-weight</span><span class="p">:</span><span class="w"> </span><span class="kc">bold</span><span class="p">;</span>
<span class="linenos">4</span><span class="p">}</span>
</code></pre></div>
<p>Early <span class="caps">CSS</span> 3 selector discussions seemingly wanted to avoid this, I guess for performance reasons? The somewhat novel alternative was to write out the entire selector, but be able to alter which part of it the rules affected with a “subject” indicator. At first this was a pseudo-class:</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="nt">label</span><span class="p">:</span><span class="nd">subject</span><span class="w"> </span><span class="nt">input</span><span class="p">:</span><span class="nd">checked</span><span class="w"> </span><span class="p">{</span>
<span class="linenos">2</span><span class="w"> </span><span class="k">font-weight</span><span class="p">:</span><span class="w"> </span><span class="kc">bold</span><span class="p">;</span>
<span class="linenos">3</span><span class="p">}</span>
</code></pre></div>
<p>Then later, they introduced a <code>!</code> prefix instead:</p>
<div class="highlight"><pre><span></span><code><span class="linenos">1</span><span class="o">!</span><span class="nt">label</span><span class="w"> </span><span class="nt">input</span><span class="p">:</span><span class="nd">checked</span><span class="w"> </span><span class="p">{</span>
<span class="linenos">2</span><span class="w"> </span><span class="k">font-weight</span><span class="p">:</span><span class="w"> </span><span class="kc">bold</span><span class="p">;</span>
<span class="linenos">3</span><span class="p">}</span>
</code></pre></div>
<p>Thankfully, this was decided to be a bad idea, so the current specced way to do this is… <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/:has"><code>:has()</code></a>! Unfortunately, it’s only allowed when querying from JavaScript, not in a live stylesheet, and nothing implements it anyway. 20 years and I’m still waiting for a way to style checkbox labels.</p>
</li>
<li>
<p><code><style scoped></code> was an attribute that would’ve made a <code><style></code> element’s <span class="caps">CSS</span> rules only apply to other elements within its immediate parent, meaning you could drop in arbitrary (possibly user-written) <span class="caps">CSS</span> without any risk of affecting the rest of the page. Alas, this was quietly dropped some time ago, with shadow <span class="caps">DOM</span> suggested as a wildly inappropriate replacement.</p>
</li>
<li>
<p>I seem to recall that when I first heard about <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/Web_Components">Web components</a>, they were templates you could use to reduce duplication in pure <span class="caps">HTML</span>? But I can’t find any trace of that concept now, and the current implementations require JavaScript to define them, so there’s nothing declarative linking a new tag to its implementation. Which makes them completely unusable for anything that doesn’t have a compelling reason to rely on <span class="caps">JS</span>. Alas.</p>
</li>
<li>
<p><code><blink></code> and <code><marquee></code>. <span class="caps">RIP</span>. Though both can be easily replicated with <span class="caps">CSS</span> animations.</p>
</li>
</ul>
<h2 id="thats-it"><a class="toclink" href="proxy.php?url=#thats-it">That's it</a></h2>
<p>You’re still here? It’s over. Go home.</p>
<p>And maybe push back against Blink monoculture and use <a href="proxy.php?url=https://www.mozilla.org/en-US/firefox/">Firefox</a>, including <a href="proxy.php?url=https://www.mozilla.org/en-US/firefox/mobile/">on your phone</a>, unless for some reason you use an iPhone, which forbids other browser engines, which is far worse than anything Microsoft ever did, but we just kinda accept it for some reason.</p>Eevee gained 3169 experience points2020-01-14T09:05:00-08:002020-01-14T09:05:00-08:00Eeveetag:eev.ee,2020-01-14:/blog/2020/01/14/eevee-gained-3169-experience-points/<p>Eevee grew to level 33!</p>
<p>Eevee grew to level 33!</p>
<p>I had kind of a rough year. Between medication issues, a lot of interpersonal tangles, and discovering ancient trauma, it feels like my head is full of static a lot of the time, and I don’t know how to create when I’m in that state. I might be able to <em>function</em>, even do rote programming work, but I just can’t <em>synthesize</em>.</p>
<p>And that sucks. I miss it. I miss writing! I barely wrote anything here all year. I’ve had a half-finished post open for months and just haven’t been able to wrap it up and get it out.</p>
<p>I’m working on it. It’s just hard.</p>
<hr />
<p>Ash and I made <a href="proxy.php?url=https://eevee.itch.io/cherry-kisses">Cherry Kisses</a> (<strong>nsfw</strong>), probably the best puzzle game I’ve designed and the most polished game we’ve released, so that was nice. I also made a <a href="proxy.php?url=https://eev.ee/blog/2019/04/20/particle-wipe-generator/">particle wipe generator</a> out of the screen wipe effect I used in the game.</p>
<p>I started on baz, a game creator meant to kinda blend the styles of MegaZeux and PuzzleScript and bitsy, but it’s yet to see the light of day.</p>
<p>I worked a lot on fox flux — adding water physics, redesigning the player sprite, inventing some new mechanics, adding a menu, refactoring to use an <span class="caps">ECS</span>-like approach, massively cleaning up my collision code, and whatnot. I also got stuck in a quagmire of trying to make push physics work how I want, but never actually got it working despite pouring weeks and weeks into it, and now the whole codebase is in a broken shambles. Kind of a mixed bag there.</p>
<p>I finally started on <span class="caps">GLEAM</span>, an editor for the <span class="caps">VN</span> engine I’ve used for Floraverse for many years now. It’s not <em>quite</em> ready for public use, but it’s far enough along that I can make VNs with it and only a little manual adjusting, which is cool.</p>
<p><a href="proxy.php?url=https://eev.ee/blog/2019/10/26/goodbye-twigs/">Twigs died.</a></p>
<p>After half a year of pulling teeth, we managed to get Ash’s divorce from Marl finalized.</p>
<p>Ash and I married.</p>
<p>I did the <a href="proxy.php?url=https://eev.ee/blog/2019/12/01/advent-calendar-2019/">advent calendar</a>, which included a dozen or so smaller projects. That was pretty fun, if a bit ambitious.</p>
<p>I drew more than the previous year, I think, and probably got better at it. I even drew some character references, at long last.</p>
<hr />
<p>I don’t know what I’ll do this year! I’m tired of listing a bunch of ambitions and then not being able to do them. But I’ll keep trying.</p>
<!-- stick this down here to keep it out of the preview -->
<p><audio src="proxy.php?url=/media/2012-01/levelup.ogv" controls autoplay></p>Advent calendar 20192019-12-01T19:48:00-08:002019-12-01T19:48:00-08:00Eeveetag:eev.ee,2019-12-01:/blog/2019/12/01/advent-calendar-2019/<div class="prose-full-illustration">
<img alt="Calendar of things I made during December, with little screenshots" src="proxy.php?url=https://eev.ee/media/updates/advent2019.png"/>
</div>
<p>🔗 <a href="proxy.php?url=https://c.eev.ee/advent-2019/"><strong>Advent calendar</strong>, with links to individual projects</a></p>
<p>Happy new year!</p>
<p>For December, I had the absolutely ludicrous idea to do an advent calendar, whereupon I would make and release a thing <em>every day</em> until Christmas.</p>
<p>It didn’t go quite as planned! But some pretty good stuff still came out of it.</p>
<div class="prose-full-illustration">
<img src="proxy.php?url=https://eev.ee/media/updates/advent2019.png" alt="Calendar of things I made during December, with little screenshots">
</div>
<p>🔗 <a href="proxy.php?url=https://c.eev.ee/advent-2019/"><strong>Advent calendar</strong>, with links to individual projects</a></p>
<p>Happy new year!</p>
<p>For December, I had the absolutely ludicrous idea to do an advent calendar, whereupon I would make and release a thing <em>every day</em> until Christmas.</p>
<p>It didn’t go quite as planned! But some pretty good stuff still came out of it.</p>
<hr />
<p><strong>Day 1</strong>: I started out well enough with the <a href="proxy.php?url=https://c.eev.ee/doom-text-generator/">Doom text generator</a> (and <a href="proxy.php?url=https://eev.ee/blog/2019/12/01/doom-text-generator/">accompanying release post</a>), which does something simple that I’ve wanted for a long time but never seen anywhere: generate text using the Doom font. Most of the effort here was just in hunting down the fonts and figuring out how they worked; the rest was gluing them together with the canvas <span class="caps">API</span>. It could be improved further, but it’s pretty solid and useful as-is!</p>
<p><strong>Day 2</strong>: I tried another thing I’d always wanted: making a <a href="proxy.php?url=https://c.eev.ee/puzzles/the-nuclear-age.html">crossword</a>! (Solve interactively on <a href="proxy.php?url=https://squares.io/info/3szq4vjmtk3e65j55uxp/info">squares.io</a>!) I didn’t expect it to take <em>all day</em>, but it did, and <em>even then</em> I found a typo that I didn’t have time to fix, and I had to rush with the clues. All in all, an entertaining but way too difficult first attempt. I’d love to try doing this more, though.</p>
<p><strong>Day 3</strong>: I’ve made a couple <span class="caps">SVG</span> visualizations before — most notably in my post on <a href="proxy.php?url=https://eev.ee/blog/2016/05/29/perlin-noise/">Perlin noise</a> — and decided to take another crack at it. The result was a <a href="proxy.php?url=https://c.eev.ee/viz/trig-functions.html">visualization of all six modern trig functions</a>, showing the relationships between them in two different ways. I’m pretty happy with how this turned out, and delighted that I learned some relationships I didn’t know about before, either! I do wish I’d drawn some of the similar triangles to make the relationships more explicit, but I ran out of time — just orienting the text correctly took <em>ages</em>, especially since a lot of it needed different placement in all four quadrants. I vaguely intended to get around to doing a couple more of these, but it didn’t end up happening.</p>
<p><strong>Days 4 and 7</strong>: I love the <a href="proxy.php?url=https://www.lexaloffle.com/pico-8.php"><span class="caps">PICO</span>-8</a>‘s built-in tracker, which makes way more sense to me than any “real” tracker, and set out to replicate it for the web. The result is <a href="proxy.php?url=https://c.eev.ee/picotracker/">PICOtracker</a>! Unfortunately, this one didn’t get fully finished (yet) — it can play back sounds and music from the hardcoded <a href="proxy.php?url=https://eevee.itch.io/under-construction">Under Construction</a> cart, but doesn’t support editing yet. Most of my time went to figuring out the Web Audio <span class="caps">API</span>, figuring out what the knobs in the <span class="caps">PICO</span>-8 tracker actually <em>do</em> (and shoutout to <a href="proxy.php?url=https://github.com/picolove/picolove/">picolove</a> for acting as source code reference), and figuring out how to weld the two together. I definitely want to revisit this in the near future!</p>
<p><strong>Day 5</strong>: I’d been recently streaming <a href="proxy.php?url=https://doomwiki.org/wiki/Eternal_Doom">Eternal Doom <span class="caps">III</span></a> and was <em>almost</em> done, and I keep being really lazy about putting Doom streams on YouTube, so I finished up the game (which took far, far longer than I expected) and <a href="proxy.php?url=https://www.youtube.com/watch?v=9Y35ga7-ndw&list=PLe3hrqBmMcMI6M_Qqem9KflrSgR6An3jS">posted the whole thing as a playlist</a>. It spans like 24 hours. Good if you, uh, just want some Doom noise to listen to in the background.</p>
<p><strong>Day 6</strong>: I’d expected Eternal Doom to be a quick day so I could have a break, and it was not. So I took an explicit day off.</p>
<p><strong>Days 8 and 13</strong>: I made <a href="proxy.php?url=https://c.eev.ee/flathack/">flathack</a>, a web roguelike with only one floor! The idea came from having played NetHack a great many times, and having seen the first floor much more than any other part of the dungeon — so why not make that the whole game? It needs a lot more work, but I’m happy to have finally published a roguelike, and I think it already serves its intended purpose at least a little bit: it’s a cute little timewaster that doesn’t keep killing you.</p>
<p><strong>Days 9–12</strong>: I got food poisoning. It sucked. A lot.</p>
<p><strong>Days 14–20</strong>: Fresh off of making flathack in only two days, I got a bit too big for my britches and decided to try writing an interactive fiction game. In one day. Spoilers: it took more than one day. But I think the result is pretty charming: <a href="proxy.php?url=https://c.eev.ee/anise-escape-despair/">Star Anise Chronicles: Escape from the Chamber of Despair</a>, a game about being a cat and causing wanton destruction, and also the first Star Anise Chronicles game to actually be published. A good chunk of the time was spent just drawing illustrations for it, which weren’t strictly necessary, but they add a lot to the game and they <em>did</em> get me back in an art mood.</p>
<p><strong>Day 21</strong>: I feel like I’ve been scared of color for a long time, and that’s no good, so I <a href="proxy.php?url=https://twitter.com/eevee/status/1208657658716143616">drew and colored something</a>.</p>
<p><strong>Day 22</strong>: I drew some weird porn, and colored it too! Porn is just a blast to draw, and it’d been a while. I’ll let you find the link on the calendar if you really want it.</p>
<p><strong>Day 23</strong>: Did not exist, due to becoming nocturnal.</p>
<p><strong>Day 24–28</strong>: I started a big reference of a bunch of my Flora characters way back in November 2018, but I tried to <em>paint</em> it when I didn’t know what I wanted in a painting style, and eventually I gave up. Flat colors are better for references anyway, so I tried again, and this time I finished! I’m really happy with how it came out — I feel like I’m finally starting to get the hang of art, maybe, just as I hit five years of trying. Again, it’s wildly <span class="caps">NSFW</span>, but the link is on the calendar.</p>
<hr />
<p>All told, I didn’t <em>quite</em> end up with 25 distinct things, but I did make some interesting stuff — some of which I’d been thinking about for a long time — and I’ll call that a success.</p>
<p>I’d love to get flathack to the point that it’s worth playing repeatedly, make more crosswords, and finish PICOtracker — but those will have to wait, since my <a href="proxy.php?url=https://itch.io/jam/games-made-quick-four"><span class="caps">GAMES</span> <span class="caps">MADE</span> <span class="caps">QUICK</span>??? <span class="caps">FOUR</span></a> jam is coming up in a few days!</p>
<p>And speaking of which, I need to put a bunch of this stuff on <a href="proxy.php?url=https://itch.io/jam/games-made-quick-four">Itch</a>!</p>