Start creating blog posts
22
examples/blog.md
Normal file
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
# slug: some-custom-slug
|
||||
title: Example Blog Post
|
||||
# pubDate: 2024-01-01
|
||||
isDraft: true
|
||||
isAgeRestricted: true
|
||||
authors: bad-manners
|
||||
# thumbnail: /src/assets/thumbnails/story_thumbnail.png
|
||||
description: |
|
||||
Some funny text.
|
||||
# posts:
|
||||
# mastodon: https://meow.social/@BadManners/133742069
|
||||
tags: []
|
||||
# series: the-lost-of-the-marshes
|
||||
# prev: previous-blog-post
|
||||
# next: ~
|
||||
# relatedStories: []
|
||||
# relatedGames: []
|
||||
# lang: en
|
||||
---
|
||||
|
||||
The blog post goes here.
|
|
@ -4,6 +4,7 @@ title: Example Game
|
|||
# shortTitle: Example
|
||||
# pubDate: 2024-01-01
|
||||
isDraft: true
|
||||
isAgeRestricted: true
|
||||
authors: bad-manners
|
||||
contentWarning: >
|
||||
This game contains some stuff.
|
||||
|
|
|
@ -4,6 +4,7 @@ title: Example Story
|
|||
# shortTitle: Example
|
||||
# pubDate: 2024-01-01
|
||||
isDraft: true
|
||||
isAgeRestricted: true
|
||||
authors: bad-manners
|
||||
# wordCount: 1000
|
||||
contentWarning: >
|
||||
|
|
BIN
src/assets/images/crossing_over/architecture_vn.png
Normal file
After Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 397 KiB After Width: | Height: | Size: 397 KiB |
BIN
src/assets/images/crossing_over/bard.png
Normal file
After Width: | Height: | Size: 9.3 KiB |
BIN
src/assets/images/crossing_over/bard_concept_art.jpg
Normal file
After Width: | Height: | Size: 116 KiB |
BIN
src/assets/images/crossing_over/boat_wakes.png
Normal file
After Width: | Height: | Size: 379 KiB |
BIN
src/assets/images/crossing_over/briefcase.png
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
src/assets/images/crossing_over/loose_thoughts.png
Normal file
After Width: | Height: | Size: 84 KiB |
BIN
src/assets/images/crossing_over/marco_concept_art.jpg
Normal file
After Width: | Height: | Size: 132 KiB |
BIN
src/assets/images/crossing_over/marco_first_appearance.png
Normal file
After Width: | Height: | Size: 348 KiB |
BIN
src/assets/images/crossing_over/marco_sprites.png
Normal file
After Width: | Height: | Size: 201 KiB |
BIN
src/assets/images/crossing_over/scene_in_final_game.png
Normal file
After Width: | Height: | Size: 274 KiB |
BIN
src/assets/images/crossing_over/script_word_count.png
Normal file
After Width: | Height: | Size: 85 KiB |
BIN
src/assets/images/crossing_over/textbox.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
251
src/content/blog/breakdown-taken-in.md
Normal file
|
@ -0,0 +1,251 @@
|
|||
---
|
||||
title: "Story Breakdown: Taken In"
|
||||
pubDate: 2024-01-23
|
||||
isDraft: true
|
||||
isAgeRestricted: true
|
||||
authors: bad-manners
|
||||
# thumbnail: /src/assets/thumbnails/story_thumbnail.png
|
||||
description: |
|
||||
First time annotating a vore story; in this case, [Taken In](/stories/taken-in). Here, I go over my writing process while offering additional tidbits of information.
|
||||
tags: []
|
||||
relatedGames:
|
||||
- crossing-over
|
||||
---
|
||||
|
||||
All in all, going over the story and breaking it down was a fun process. This was originally a text document, which I had to completely refit into this blog post that you're reading (oof...!). But for the sake of posteriority, I think it was worth the effort.
|
||||
|
||||
If you're up for this non-linear read, then I hope you enjoy this sneak-peek into my thoughts! Just hover or long-press over the links with dotted underlines, and it should show the relevant annotations. Now, without further ado, let me reintroduce...
|
||||
|
||||
<h2><a class="decoration-dotted" id="note-1" href="#note-1" title="I usually try to make titles with a double meaning. Here, 'take in' usually means bringing a pet to one's home - in this case, it's more like the 'pet' has forced itself into the PoV's place! Plus, the PoV is taken into Sam's body." data-tooltip>Taken In</a><sup>1</sup></h2>
|
||||
|
||||
<a class="decoration-dotted" id="note-2" href="#note-2" title="I actually had to rewrite most of the intro from scratch, it got too wordy and just boring to read. For a PoV segment, I thought something less detailed and to-the-point was more fun." data-tooltip>Clank! Shuffle! Crunch!</a><sup>2</sup> The sounds outside are too loud to be stopped by the walls in your room, and you jolt awake.
|
||||
|
||||
Ugh, and you were finally in deep sleep...
|
||||
|
||||
<a class="decoration-dotted" id="note-3" href="#note-3" title="With PoV stuff, even details like 'when you wake up in the middle of the night' are better left ambiguous." data-tooltip>You check the time, and it's still too early to be out of bed</a><sup>3</sup>. The sun hasn't even risen yet. But the commotion continues outside, forcing you to get up and investigate.
|
||||
|
||||
It's probably some wild animal messing with the trash, you think to yourself. <a class="decoration-dotted" id="note-4" href="#note-4" title="Again, leaving 'what the place is' ambiguous so it could be a house, an apartment, etc." data-tooltip>You navigate through your place in the dark and out the door</a><sup>4</sup>, too tired to worry about your own safety. You don't even bother to lock it behind you as you leave.
|
||||
|
||||
There's nobody outside, with only dim streetlights to illuminate the empty street. It seems you're the only one who bothers to investigate the noise, and it definitely sounds like it's coming from a metal trashcan. Or more precisely, from something messing with it, with no regard for the neighborhood's peace and quiet.
|
||||
|
||||
When you get close enough, <a class="decoration-dotted" id="note-5" href="#note-5" title="A lot of this was having fun with how I would look and act as a feral. Despite the narration following the PoV, I still wanted to make my sona's actions credible, even if the intentions are omitted. In this case, Sam is just digging through trash, looking for something interesting to play with." data-tooltip>you find some animal with its upper half inside of the metal bin</a><sup>5</sup>. It's too big to be a raccoon, or even a fox. You can only see its long, furry legs and tail sticking out from the cylinder as it continues to nose its contents.
|
||||
|
||||
You can't tell what species it is without looking at its face, but the feral might be a dog or a wolf. Its coat is quite unusual, green with white patterns at the back. The paws and the tip of its tail are also colorless, except for its bright green pawpads, and some light teal spots between body and limbs. An unusual coloration, for sure – you imagine that the tones don't offer the creature much in terms of camouflage, out in the wild...
|
||||
|
||||
As it continues to scavenge the trash can for food, and make a ruckus while at it, you slowly approach it without any sense of danger. It doesn't really give wolf vibes, so maybe it's just a weird breed of dog? If it's an escaped pet, it might be wearing a collar — hopefully, it'll have the owner's phone number engraved on a tag.
|
||||
|
||||
"Easy, boy..."
|
||||
|
||||
You call out in a whisper, instinctively assuming that the animal is a 'he'. The slim and fuzzy creature stops, <a class="decoration-dotted" id="note-6" href="#note-6" title="Sam has great hearing thanks to his large maned wolf ears." data-tooltip>hearing your groggy voice even while constantly bumping against the metal walls</a><sup>6</sup>, and pulls himself out from the can.
|
||||
|
||||
As you step back, you can see his front side when he turns on all fours towards you. The front paws are similar to the hind ones. Definitely canine, with a gradient that goes from white to green, and teal at the middle. Sadly, <a class="decoration-dotted" id="note-7" href="#note-7" title="Always fun to pretend the briefcase is covering my face. I also played with that concept in the story 'Part of the Show'." data-tooltip>you can't get a good view of his face, as he's carrying a sizable object with his snout</a><sup>7</sup>. Something rectangular, which he must've fished out of the trash. Your tired eyes have to adjust to the bad lighting in order to process the blurry image, but the defining features of the gray texture and the handle at the top clear up any confusion.
|
||||
|
||||
"Is that a...metal briefcase?" You ask yourself, and <a class="decoration-dotted" id="note-8" href="#note-8" title="I never decided if Sam actually understood what the PoV spoke or not, despite being sapient. So I left that ambiguous, even for myself. Whatever the case, here he just acts curious about the PoV." data-tooltip>the strange dog simply tilts his head to the side</a><sup>8</sup>.
|
||||
|
||||
The container hiding his head also rolls along, only letting you spot his large ears. They are too big for a dog, so his species can't be that, either. Maybe a maned wolf, then...? It'd certainly explain why he's got such long legs...! But they aren't from around here, you're pretty sure. He might be an exotic pet that has escaped, one without a collar. <a class="decoration-dotted" id="note-9" href="#note-9" title="Normally I write the reasons after I decide what happens. In the case of PoV stories, it's easier since reasons don't have to necessarily follow or be consistent. For example, the narrator could be mistaken - like in this case." data-tooltip>Maybe he was looking for food, and smelled something tasty inside of the briefcase</a><sup>9</sup>.
|
||||
|
||||
"It's okay, boy..." You slowly approach him with a cutesy voice, careful that he might get aggressive, drop the case, and bite you. Do maned wolves even get rabies?
|
||||
|
||||
Your bluff about him being domesticated seems right on the money, <a class="decoration-dotted" id="note-10" href="#note-10" title="Sam just acts like a silly dog who wants to play." data-tooltip>as he doesn't seem alarmed by you at all. He sits down, slowly wagging his tail like a huge brush as he eagerly awaits</a><sup>10</sup>. <a class="decoration-dotted" id="note-11" href="#note-11" title="Sam can see with his entire mimic face. In his case, I like to play with that eerie feeling you get when you're being observed, but without any eyes staring back at you (see the story 'Part of the Show' I mentioned earlier)." data-tooltip>You can't see his eyes, but it definitely feels like he's staring at you</a><sup>11</sup> – even with the metal box in the way.
|
||||
|
||||
One of your hands moves to the back of his head, holding the base of his ear. His fur is so silky and warm! You can't help it but give it a few rubs. He seems to immediately like your scritches, thumping one of his legs against the floor and panting excitedly. He certainly sounds just like a dog, making you wonder if maned wolves also bark like them.
|
||||
|
||||
"Good boy..." You whisper to him, stopping the scritches <a class="decoration-dotted" id="note-12" href="#note-12" title="You can pet the Sam! This was one of the cheeky images I really wanted to include. My stories have plenty of self-serving moments like this scattered around." data-tooltip>to give his head a couple of pats</a><sup>12</sup>.
|
||||
|
||||
He replies with a happy whine, and your hand brushes against the back of the briefcase. He still hasn't let go of his prized possession. Either that, or – you think to yourself – it may be stuck to his face... Poor thing, that must be it! Of course, domesticated or not, the best thing that you can do for the maned wolf is get his face free. <a class="decoration-dotted" id="note-13" href="#note-13" title="Poking fun at the absurd line of reasoning. The reader is likely aware (either from seeing my sona or reading the tags) that the plan won't work, so there's a big chance they're in on the joke." data-tooltip>That's simply the logical line of reasoning</a><sup>13</sup>.
|
||||
|
||||
You squat to his height. Your other hand approaches the friendly canine, and each one holds the aluminum briefcase by the edges. Then you grasp it and start pulling slowly. It's weird, the metal almost feels warm to the touch...
|
||||
|
||||
His head moves along with it, and <a class="decoration-dotted" id="note-14" href="#note-14" title="Sam growls because you're pulling on his face! But it's sturdy enough that it doesn't actually hurt him, it's just bothersome." data-tooltip>he starts growling like a dog that doesn't want to let go of his toy</a><sup>14</sup>. You mentally apologize for the temporary discomfort you might be causing, but the briefcase is stuck harder than you thought. If so, this unfortunate canine could've been starving for hours, or more...! His growls don't matter – it will be over soon, once you manage to get this blasted thing off.
|
||||
|
||||
You start pulling harder, but the briefcase simply won't budge! In turn, <a class="decoration-dotted" id="note-15" href="#note-15" title="Being a mimic, Sam communicates with sounds he's learned. Even as he speaks, he doesn't use his vocal chords! Which means he can communicate even with prey blocking his throat." data-tooltip>the maned wolf growls harder at you, and then...hisses at you? Isn't that more of a feline thing...?</a><sup>15</sup> Regardless, the aluminum case doesn't seem to move an inch. Even though it feels stuck to the creature's face, it almost seems to bulge forward as he continues to hiss...
|
||||
|
||||
"...Wh-What?"
|
||||
|
||||
It's hard to tell under the awful streetlight, but for a moment, out of the corner of your eye, you swear that you just saw something, slithering in the middle of the maned wolf's face...
|
||||
|
||||
The slight distraction causes you to lose your balance, falling back and pulling the maned wolf on top of you. Lying flat against the hard sidewalk, his canine forepaws pin your chest down. He's not too heavy that it hurts, but you notice how he's about the same size as you. But something more urgent calls your attention. He still has that briefcase on his head, almost against your own face – and you can see an opening in the middle.
|
||||
|
||||
<a class="decoration-dotted" id="note-16" href="#note-16" title="I hardly display my maw if I can help it. But here I want to give you some affectionate face licks, to show I'm just being playful - not realizing my actions will be misunderstood." data-tooltip>The metal deforms and bends, not unlike skin or some other organic tissue. A symmetrical slit begins to expand and part the rectangular surface into an upper and a lower half. A pair of jaws. It opens just enough to reveal a set of long and jagged teeth along the vertical direction – and a yellowish tongue that looks more like an octopus tentacle without its suckers. The maw twists into something like a smile, and the maned wolf wags his tail as the slobbery appendage drags over your face.</a><sup>16</sup>
|
||||
|
||||
Panic sets into you. Suddenly face to face with monstrous mandibles, <a class="decoration-dotted" id="note-17" href="#note-17" title="His teeth are dull, so this is another case where the narration is purposefully wrong. I tried to lean heavily into the unwillingness of the PoV with these descriptions." data-tooltip>your instincts tell you to fear for your life before those razor-sharp fangs can sink into your skin</a><sup>17</sup>. You squirm and struggle, trying to get out from underneath this 'maned wolf'.
|
||||
|
||||
He makes no effort to keep you down, and slips to one side while you roll to the other. The creature yelps before falling with his back against the trash can, knocking it over with <a class="decoration-dotted" id="note-18" href="#note-18" title="A bit of a self-joke, since that's the sound I make when I (purposefully) bang my face against a hard surface. It's become a bit of a joke greeting, similar to kobolds yipping and dogs murring - meanwhile I go 'clang!'." data-tooltip>a loud 'clang!'</a><sup>18</sup> that rings in your ears. Adrenaline rushes through your bloodstream, and you quickly get up to run back to your door.
|
||||
|
||||
Maybe the beast wasn't trying to eat you. But that thought immediately gets shoved to the back of your mind, as you are still too frightened to think about whether you should've given it a second chance. Right now, your only priority is making your way home, and locking the door for safety.
|
||||
|
||||
Each step as you sprint feels like it's in slow-motion. You don't even pay attention to whether there's anyone else around the street to cry out to, and running seems like your best call of making it out of this hunt.
|
||||
|
||||
Finally, you're back at the entrance to your place. Relief washes over you... <a class="decoration-dotted" id="note-19" href="#note-19" title="In my head-canon, Sam doesn't get angry at the PoV for pushing me, thinking that it was a mistake. But seeing them run away makes him think that the PoV is playing tag." data-tooltip>But suddenly, something grabs your body and constricts you like a lasso.</a><sup>19</sup> You lose your balance and fall forward.
|
||||
|
||||
"Oof...!"
|
||||
|
||||
Thankfully, the appendage coiling around your chest and arms cushions your fall. You can hear excited murrs behind you. It's clear that the creature gave chase after you, catching up before you could make it inside – and, quite literally, <a class="decoration-dotted" id="note-20" href="#note-20" title="My tongue is very helpful, able to grab and lift heavy objects with ease." data-tooltip>catching your fall with his tongue</a><sup>20</sup>. It feels so slimy and fleshy, drooling over your constrained ribs. You struggle some more, but the tentacle's grip is almost like a vise's.
|
||||
|
||||
Then, <a class="decoration-dotted" id="note-21" href="#note-21" title="Feral Sam thinks vore is just another kind of play! He thinks everyone likes being squished in his stomach, even if that's more of an exception than a rule." data-tooltip>you can feel warm slobber much further down your body. The tip of your feet are quickly engulfed by his maw – that terrifying, ravenous opening</a><sup>21</sup>. You don't even look, squirming harder and trying to pull your legs free. He keeps his tongue around you, preventing you from getting up again. Your ankles brush against the inside of his maw, wet flesh against your skin. His mouth is as wide as the briefcase he has for a face. It has no issue squeezing your limbs towards his throat.
|
||||
|
||||
"H-Help..." Your voice comes out weak after the initial shock. Your only choice now is to hope a passerby can hear your pleas. "Someone! Please help–pmMFmf...!"
|
||||
|
||||
Before you even have the chance to shout, <a class="decoration-dotted" id="note-22" href="#note-22" title="...Although he can get too into his predatorial role. Even if you are willing, chances are he will still fall into his instincts and try to restrain you. His tongue definitely helps with that!" data-tooltip>something gags you. It's the yellow tip of the creature's incredibly long tongue</a><sup>22</sup>, forcing itself between your jaws and pinning down your own tongue! Yuck... It tastes like saliva – but also citric? Not what you expected...
|
||||
|
||||
Then again, having a beast's tongue pry your lips open as it devours you alive was the last thing you'd expected when you left to check the noise outside your place.
|
||||
|
||||
His surprisingly versatile muscle does an unfortunately great job at keeping you quiet and still, helping the voracious animal consume you whole. ...Which does _seem_ weird, and begs the question. If it wanted to devour you, why go to such lengths to immobilize you and eat you alive?
|
||||
|
||||
First of all, <a class="decoration-dotted" id="note-23" href="#note-23" title="Same size prey are Sam's favorite, even if they are harder to devour. Though he will usually nom prey smaller than him." data-tooltip>you're too big. There's no way it can swallow you whole...right?</a><sup>23</sup> Besides, with the daggers protruding from his monstrous gums, he could have shredded you to pieces. But even as you feel the pearly spires brushing against your lubricated calves while struggling, his tongue seems to do its best to guide you through the gap between them, leaving you unscathed.
|
||||
|
||||
Unable to do much, you look back at it. The sight of your feet disappearing between the two rows of fangs is terrifying – but you can feel them in his throat. When you struggle, you catch a glimpse of bulges moving in his neck. His back legs are straightened vertically, and his forepaws are low against the ground. This twisted maned wolf almost looks like a playful pup...and the tail wagging wildly behind him seems to confirm it. He seems to be eating you alive not just for food, but for fun.
|
||||
|
||||
Well, you can't say you're sharing in any of that fun. You thrash and grunt helplessly, yet you continue to gradually vanish within the lime green confines of the unnatural canine. The bulge in his neck slowly shifts down to his chest and belly, distending his slim abdomen with your own limbs. The dry, clean appearance of his furred midriff doesn't reflect the sensations around your feet – it's hot, slimy, and tight inside. He shouldn't be eating you... He _can't_ be eating you! You aren't meant to be food!
|
||||
|
||||
Keeping you down on the ground, he slowly scoots closer, maneuvering his parted jaws around you to claim you. <a class="decoration-dotted" id="note-24" href="#note-24" title="I wanted to give the PoV some agency to make the vore scene more interesting, in contrast to my previous, more passive attempts at PoV prey. It also elevates the unwillingness and the pred's cluelessness." data-tooltip>One of your arm slips free from his tongue's grip</a><sup>24</sup>, and you reach for the first thing you see – your doormat. You grab it and toss it against your assailant, but it misses and flops pathetically to the side.
|
||||
|
||||
The briefcase-faced creature is completely unfazed, simply fitting the last of your legs into his mouth with more gulps. He begins working up your hips with no signs of stopping, and your knees get forced through his cardiac sphincter, bending to fit more into his tight stomach. It's surprising (and disturbing) how much his can distend to fit a large prey, just like a snake.
|
||||
|
||||
His tongue seems too busy plugging your mouth, making sure that you aren't making any sounds that may alarm your neighbors. Though considering how no one bothered to investigate all the noise earlier, it seems unlikely to you that anyone would even respond to your calls for help.
|
||||
|
||||
Regardless, you still have a free hand, and you tap around the ground for anything else that you can grab onto or throw at him again. It's hard to move your eyes or focus, but you can't really find anything. Your time is running short. Unless you want to become maned wolf food, you need to think of something.
|
||||
|
||||
With his tongue being the only thing you can reach, you grab back the slick appendage that was just holding your arm fast. It's smooth and slippery, and not really a pleasant texture to have between your fingers. It's hard to keep your grip on it, but you pull on it and try to get it out from your mouth.
|
||||
|
||||
The beast only seems to enjoy the fight you're putting up. <a class="decoration-dotted" id="note-25" href="#note-25" title="Again, Sam isn't restricted to only the sounds a maned wolf would make - in fact, he's more likely to mimic a dog's bark than a maned wolf's one. (Seriously, have you heard a maned wolf's bark? It sounds like a bark in reverse!)" data-tooltip>It starts making sharp, repeated noises – a hyena's laughter, perhaps...?</a><sup>25</sup> As he mimics yet again the sounds of a completely different animal, he keeps wolfing you down as quickly as before, or maybe even faster.
|
||||
|
||||
Your other arm, pinned against your ribs, sinks in against the squishy floor of his maw. The touch of tight walls and warm saliva reaches your fingertips. Your lower body has been crammed into his belly, squeezed as compactly as it can be. Despite your size, <a class="decoration-dotted" id="note-26" href="#note-26" title="Sam could eat prey larger than him - if they are cooperative enough! This sentence is also a play on the story's title." data-tooltip>you fear that he may have just enough room to take in all of you.</a><sup>26</sup>
|
||||
|
||||
With your upper body slipping down the back of his maw, he's forced to readjust the serpentine coil of his tongue. His entire pose shifts, craning his neck upwards and standing tall on all fours to lift your head off the ground. With gravity on his side, the constant tug from his throat is more potent. <a class="decoration-dotted" id="note-27" href="#note-27" title="Sam can control each section of his tongue independently." data-tooltip>His greenish tongue moves up your body, wrapping itself around your face and slobbering you as it steals your flavor... All without its tip ever leaving your mouth.</a><sup>27</sup>
|
||||
|
||||
You continue to pull on the probing muscle. Your chances of breaking free on your own are nearly null. No matter how stretched his stomach might already be, the beast likely won't be satisfied until the rest of you slides down the slope of his esophagus. Your last recourse is to call for help, one last time.
|
||||
|
||||
His jaws relax a bit after your shoulders squeeze into his gullet, and his swallows only get louder and closer to your ears. Your head is still smooshed by the tongue wrapped around it, slithering like a tentacle across your skin. Your whole body is drenched in his slobber by now. It's getting tighter around your arm, and thus harder to keep trying to yank his muscly appendage out.
|
||||
|
||||
But for a moment, its unforgiving strength wanes. You finally manage to yank the appendage and ungag yourself...! But you need a moment to sputter all of his drool from your mouth before you can speak. Once it's clear, you can yell.
|
||||
|
||||
"S-Somebody–!"
|
||||
|
||||
Unfortunately, it's too little too late. This single moment cut short by a snap of his jaws, his teeth clacking less than an inch away from your face. The frail light from the lamppost turns into complete darkness, and your gaze is confronted with the squelching, hungry reality of your situation.
|
||||
|
||||
With just a single gulp, you slip away from his maw entirely.
|
||||
|
||||
The peristaltic tunnel hugs the contours of your face even more greedily than his tongue did. It feels like being shoved into a stuffy bag – a wet, tight, groaning bag. You still want to shout for help, but a part of you knows full well that your words will fall on deaf ears. You lost, and the animal has claimed his quarry.
|
||||
|
||||
Compressed into the tightest ball that your flexibility allows, your upper half curls past the bend at the bottom of his gullet. It's not a loose fit, but at least the pressure doesn't hurt. The tautened wrinkles of his stomach massage your muscles, making sure that your body won't be sore from the stress that the walls inherently force onto you. That's one less worry for your stay in his organ, at least – though now your worries shift to how long that stay might be.
|
||||
|
||||
<a class="decoration-dotted" id="note-28" href="#note-28" title="Sam doesn't ever digest his prey, but the PoV doesn't know that. I'm exclusively a non-fatal vore writer, but I'll play around with mentions of digestion when it fits the narrative. In this case, it comes back to the prey's unwillingness - and any reader that has checked the tags beforehand, or is familiar with my writing, knows that no digestion will occur." data-tooltip>Given what stomachs normally excel at, your prospects are not great, admittedly. The walls, probably green-hued like his tongue, churn around you with careless abandon, likely treating you no different from a large clump of food. Despite that, the damp air is breathable, and the slime sluicing around you doesn't cause you any harm. Maybe this creature can't really digest you, or maybe it won't. You can only guess – and hope.</a><sup>28</sup>
|
||||
|
||||
Nevertheless, the creepy-looking canid starts walking – it's easy to tell when the chamber sloshes and sways with your weight. You can hear his happy heartbeat as he heads somewhere after his satiating meal. <a class="decoration-dotted" id="note-29" href="#note-29" title="He wasn't actually looking for food. Plus, my vision was for the vore to be clean, except for drool and slime to accentuate the possessiveness of the predator over his prey." data-tooltip>Thankfully, there isn't anything else in his stomach other than you and his sluices. His trashcan scavenging must have been a flop, you figure...</a><sup>29</sup> And you are (relatively) lucky to have been the only thing he has eaten tonight.
|
||||
|
||||
<a class="decoration-dotted" id="note-30" href="#note-30" title="As is revealed later, after voring the PoV, Sam walks through their half-open door and lies on their bed." data-tooltip>Suddenly, the movement stops, and you are flipped against your side. The whole place shifts as the ravenous animal lies down on his side, keeping his extremely bulged out stomach against a soft surface.</a><sup>30</sup> You can hear when he starts purring like a cat, rumbling the walls around you in his pleased state.
|
||||
|
||||
As if all of this already isn't demeaning enough, something outside of his stomach starts pressing and rubbing the wall against you. It's too small and long to be one of his paws, and it makes a subtle 'shlick... shlick...' noise each time that it presses into his fur... <a class="decoration-dotted" id="note-31" href="#note-31" title="Also another self-indulgent image I wanted to include. I love the mix of possessiveness and affection when a pred licks their prey-filled belly." data-tooltip>Is he licking his own belly?</a><sup>31</sup> Why would he do such a thing? Is he trying to comfort you...?
|
||||
|
||||
Well, it would certainly explain why you saw him wagging his tail playfully earlier – although you were more focused on escaping his metallic lips –, and why his body isn't interested in breaking you down like food. This is all simply dumb fun for him, at your expense. A twisted entertainment for a twisted being. At one point, you considered that you'd judged it too harshly... But then again, you _did_ end up in the belly of the beast, and you don't really have a choice except to trust in his good intentions – if he really has any.
|
||||
|
||||
The licks seem to stop, and you figure that you might as well try talking to it. He may be feral, but with the artificial-looking face of his, he's not your usual feral. Perhaps he can understand language.
|
||||
|
||||
"H-Hey, uhh, boy..." You speak out, trying to avoid the gastric mucus from getting onto your mouth. "You maned wolf...briefcase...thingy. Can you hear me?"
|
||||
|
||||
His body shakes around you briefly, reverberating with a soft cat-like trill. That doesn't really say much about his sapience.
|
||||
|
||||
"Umm... Do you think you can let me out? Please...?"
|
||||
|
||||
Silence. You try to adjust your position, but the walls cradle you tightly. You ask him again, but you can only hear the idle sounds of your predator's body. Then, you finally hear something else in return: his gentle, cute snores. <a class="decoration-dotted" id="note-32" href="#note-32" title="Who can resist a good nap after a hefty meal...? Either way, Sam thinks his prey will also relax in his belly and enjoy a nice night's sleep inside of his stomach." data-tooltip>He fell asleep with you in his stomach...</a><sup>32</sup>
|
||||
|
||||
Well, then. This isn't good. It could be worse – but matter of fact, it seems that you're stuck in this creature for now. Your limbs are compressed, so there isn't any way of struggling or escaping on your own. Stuck in a stomach, against your volition. You can only hope that his organ won't get active while he's unconscious. Other than that, there's nothing you can do. All that you can do is wait and see.
|
||||
|
||||
<a class="decoration-dotted" id="note-33" href="#note-33" title="This story was meant to have full-tour from the start, but it did end up feeling like two stories stitched together. I was considering breaking it into two shorter stories, but I feel that would defeat its purpose. Still, I personally prefer the first half over the second. Especially since I've never actually gone and written an actual full-tour scene before (only the beginning, or the end; never the FULL full tour)." data-tooltip>\* \* \*</a><sup>33</sup>
|
||||
|
||||
Despite the creature's peaceful slumber, and how tired you feel, you can't really catch any sleep. Forced into a fetal position, you are constantly disturbed by rolling wrinkles and their low groans. The air is heavy and sour, even though it's breathable. Slime keeps clinging to your skin, unable to be brushed off as it coats the entire chamber.
|
||||
|
||||
Time doesn't seem to move, and you can't tell if it's been minutes or hours. By now, you wish that you'd been asleep all along, that this had all been a nightmare you'll simply wake up from... But it was all blatantly real – the creature with an unprecedented appearance really had consumed you whole. <a class="decoration-dotted" id="note-34" href="#note-34" title="Here's the biggest reason why I wanted to make the prey unwilling. By giving the PoV a motivation - escaping from Sam -, I figured that the second half could be more dynamic, giving them more to do than simply watch the full tour unfold itself around them. It was still a gamble, since it might not suit the reader's preferences, but I think this specific story wouldn't work quite as well if I had taken a more passive approach to the PoV." data-tooltip>If only you knew what it was, you could figure out how to escape its clutches. Maybe.</a><sup>34</sup>
|
||||
|
||||
The walls turn and squish, constantly creating a vacuum between the flesh and your skin, making squelches when the gaps are filled with gastric sluices. It's a persistent suction along your trapped body. If this wasn't his literal stomach, you could mistake it for some weird massage, where his inactive acids served as some acrid lotion.
|
||||
|
||||
Suddenly, the walls stop and churn louder. The low sound isn't pleasant, especially from inside. It shakes your whole body with its rumble. The walls uncomfortably contract and smoosh you tighter. You fear that this is finally the moment when the creature's nefarious intentions get revealed, <a class="decoration-dotted" id="note-35" href="#note-35" title="As much as I wanted to use 'imagined imminent digestion' as a plot point, I ended up rewriting this specific part a lot. I didn't want to be too graphic in my descriptions, and alienate people who dislike digestion like myself." data-tooltip>and his digestive system will make short work of you.</a><sup>35</sup>
|
||||
|
||||
You feel a shift at the parts furthest from the stomach's entrance. Suddenly, there's some new space manifesting at the bottom, beside your butt. Your body is slowly eased into the opening by the muscles, and you can feel the heat and smoothness creeping up <a class="decoration-dotted" id="note-36" href="#note-36" title="Standard PoV ambiguity to broaden the definition of 'anthro'. No fur/scales/feathers, only skin. No paws/wings/talons/etc., only hand/feet. And no mentions of tails, either." data-tooltip>your skin</a><sup>36</sup>.
|
||||
|
||||
Knowing that you aren't being digested is a huge relief but it doesn't change your situation. If anything, it only means you'll be sent even deeper into this feral's guts. <a class="decoration-dotted" id="note-37" href="#note-37" title="In my head, Sam's intention was to keep the prey in his stomach for the whole duration of their stay. Having them slip into his intestines is more of an accident." data-tooltip>After the stomach comes the intestines, of course – which doesn't give you much comfort.</a><sup>37</sup> <a class="decoration-dotted" id="note-38" href="#note-38" title="This is something I think I've read about on vore Twitter, but I've never actually verified it myself, so I added it as a little nod to that random memory that might be incorrect. lol" data-tooltip>Supposedly, this initial part of the long tubes should be even more efficient at breaking down food than the first chamber.</a><sup>38</sup> But your experience so far tells you it won't be that simple, for better or worse.
|
||||
|
||||
<a class="decoration-dotted" id="note-39" href="#note-39" title="I haven't really done a lot of research into the gastrointestinal system for this story, so the descriptions might not be accurate. But since I was committed to the full tour part, I wanted to make each part different visually to show the progress throughout the story." data-tooltip>More and more of these new walls, taut and smooth, replace the cushy folds from before.</a><sup>39</sup> Since your predator is about your size, there isn't much physical space in here for you. It feels less like you're actually slipping deeper, and more like his whole body is expanding and contracting, readjusting itself around you.
|
||||
|
||||
Like the rest of his anatomy so far, the snug spot is stretched way beyond an ordinary creature's capacity. The walls of the duodenum, much tighter and more uniform, are coated in that same pervasive slime from before. Overshadowed by the myriad sounds of his gastrointestinal system, you hear something akin to a purr. Your captor is surely having pleasant dreams with the large mass hidden within him, adding even more insult to injury.
|
||||
|
||||
After your head passes through the pyloric sphincter, there's thankfully no huge discomfort. Still, this place manages to be worse than the last. It's just as loud in here, the flesh has even less give, and the juices slathering your body are thicker.
|
||||
|
||||
Your only hope really seems to be waiting it out, making more progress slowly but surely. But how long is that going to take? ...How long has it already taken? What if the creature wakes up, and forces you to stay even longer?! That won't do! Since there's no help on the way, your best bet is to try and exit before he wakes up.
|
||||
|
||||
With nothing to lose, you try to squirm again. Unexpectedly, the walls clearly shift in turn. They are slippery enough, and your movements help the muscles' natural disposition to push chyme deeper in. It's still sending you the opposite way from the closest exit – but at least you manage to find a sliver of agency.
|
||||
|
||||
It's still a cramped and uncomfortable fit. More of the sluices slobber you up, not ruining your skin more than it already has been ruined, yet lubing it up in turn. Your squirms continue to dig a path, slightly making headway past the entrance of his intestines.
|
||||
|
||||
<a class="decoration-dotted" id="note-40" href="#note-40" title="I pictured that it wouldn't be the first time feral!Sam has passed a large prey through his body - which explains his actions after he wakes up, later on." data-tooltip>The tunnel groans, likely from being filled past its any reasonable limit with something that shouldn't be there – a living being, big and squirmy.</a><sup>40</sup> The fact that you are even conscious to experience all of this is a tragic miracle.
|
||||
|
||||
With enough back and forth, the texture changes around your feet. Instead of smooth, it's bumpy and soft, like a bunch of squishy buds. Fidgeting like this is a tiring process, but you don't imagine yourself getting any rest as long as you're surrounded by the hostile creature. You slip into the small intestines, and instead of muscles lined with gooey mucus, there are many villi brushing against your limbs.
|
||||
|
||||
Like myriad tendrils, the walls brush and tickle your tightly compacted body, now nestled in the tightest part of the tract. There's a more noticeable peristalsis from the walls compared to the duodenum, intended to guide whatever ends up in here through the long and winding path of absorbent tissues. It's at this stage that his body finally agrees about how you don't belong here – and its autonomous movements draw you through the tortuously long journey that will follow.
|
||||
|
||||
You try to wriggle, but your mind and body are in in a numb haze. Less so from the environment – which is equally distant from amenable and intolerable –, and more from your lack of energy and sleep. Whatever vigor you can muster is spent fidgeting, to push yourself through seemingly unending flesh.
|
||||
|
||||
<a class="decoration-dotted" id="note-41" href="#note-41" title="The second part of this story was much more wordy, with loose internal thoughts and repetitive descriptions. Basically, stuff that I added in a first pass, thinking they'd be cool additions. But most of them felt pointless and dragged down the pace of an already long section. Normally, when I edit, the total word count increases by about 5% in average, as I clarify certain passages and add missing bits. For this story, the word count during editing actually went down, from 6k to 5.9k - which is the first time that happened in writing for me." data-tooltip>The slimy lubrication on your skin slowly rubs off on the villi, making your skin less sticky as it's recycled back into the body.</a><sup>41</sup>
|
||||
|
||||
You are unable to fall asleep, no matter how much your physiology craves it. But the harsh environment forces your hand with its unsettling cacophony. Sore and tired, your muscles continue to thrash within his muscles, but the progress is literally palpable from the many protrusions lining the walls moving past your head.
|
||||
|
||||
From outside, <a class="decoration-dotted" id="note-42" href="#note-42" title="Wanted to play a bit with my mental image of a large prey forced through guts, without the bulges looking any different to an outside observer. Keeping the prey stuck in the same position for all of it was part of that objective, albeit a challenging limitation in itself." data-tooltip>it wouldn't seem like much has happened after you'd been consumed – just a person-sized bulge occasionally moving a few inches here and there</a><sup>42</sup>. But after many bends and twists, the walls relent a little, stretching further out as you approach the end of the small intestines. <a class="decoration-dotted" id="note-43" href="#note-43" title="I did make the small intestines part short on purpose, even if it is technically the longest, since there's only so much you can describe without being too repetitive. Thankfully, there's no need for the story's length to reflect the actual passage of time - it's all for the sake of making the story interesting, after all." data-tooltip>The worst leg of the journey is finally over...</a><sup>43</sup> Though other than a thinner coat of slime than before, and extreme exhaustion, there's not much to show for it.
|
||||
|
||||
Then, you hear a musical sound, muffled by flesh as it comes from outside. But you know this ringtone, it's the alarm going off on your phone. Has it been this long already...? At least, you have an idea of how many hours you've spent trapped in these detestable tunnels.
|
||||
|
||||
Still, how come you can hear your alarm? You thought you'd left your phone at home when you went out to check the noise. Unless... Oh! The dots finally connect in your fatigued head. The creature must've made his way into your room in order to sleep.
|
||||
|
||||
The briefcase-faced animal stretches on your mattress, and the intestine clenches you as he yawns. His most minute stirs cause everything to shift around. The alarm has clearly caused him to awaken, and without you to turn it off, you two might be listening to it on repeat for who knows how long.
|
||||
|
||||
Your surroundings move again, and then the phone goes silent. What?! Did he–? ...What <a class="decoration-dotted" id="note-44" href="#note-44" title="Yep, can't forget my cheeky secondary signature! I've tried to include the word 'manner' or variations of it in every story, and the ones featuring my sona are no exception. In order to avoid it from being too distracting, I make sure it only appears once. There have been times I forgot to add it to the text until I edited them, but I try to insert them in the initial writing so it doesn't feel too ham-fisted. But in this case, this one got moved around from a usage in the first half (specifically, when I mention Sam making cat noises in the trash can scene) to here. Partially because it felt more natural, and partially because I like using it when referring specifically to my sona or another of my OCs, since it's so fitting!" data-tooltip>Manner</a><sup>44</sup> of beast even knows how to even disable an alarm?
|
||||
|
||||
"H-Hey!" You call out, realizing the creature may be sapient after all. "Can you let me out?"
|
||||
|
||||
<a class="decoration-dotted" id="note-45" href="#note-45" title="Feral or no, he does enjoy having the prey in his intestines. As he wakes up to find his prey deeper than before, he decides to enjoy himself with this nice morning surprise, not really minding that things didn't go to plan." data-tooltip>The feral doesn't reply, except by slumping his weight onto you. It makes things harder and more flustering. He simultaneously pants like a dog and purrs like a cat, seemingly enjoying your mass lodged deep in his intestines.</a><sup>45</sup> Thankfully, the walls continue to gently squeeze you further out rather than in, and with the promise of freedom lurking so closely, you keep squirming against his tight flesh.
|
||||
|
||||
Finally, you feel your ankles brush against another sphincter. It's gotta be the bend leading to his large intestines! Unfortunately, it seems closed off, thanks to the tunnel being pinched by him lying on his belly. No matter how much you struggle, there's no way you can make any headway without his cooperation.
|
||||
|
||||
"I-I'm stuck...!" You whisper to yourself in frustration. "Please..."
|
||||
|
||||
<a class="decoration-dotted" id="note-46" href="#note-46" title="Another instance of me not deciding on making Sam capable of understanding the PoV or not. At the very least, he can tell the prey is frustrated - and having pushed other prey through his whole body before, he realizes this and decides to help their 'new friend'." data-tooltip>As you bemoan, the creature seems to acknowledge this – with a gloating bird's chirp, no less. Everything shifts and sways as your predator turns over, no longer grinding his belly against the mattress. Instead, two objects squeeze the walls near the top. You imagine that the beast is kneading his own guts with his forepaws. It temporarily takes away some precious space, but dislodges you through bowels once more.</a><sup>46</sup>
|
||||
|
||||
You wonder if your captor even understood your plea, but he appears to agree that you've overstayed your 'welcome'. You're in no position to deny this improbable aid, but eager to finally leave the winding intestines behind.
|
||||
|
||||
Finally, it's time to go spelunking into his large bowels, the last of the tunnels. His massage spills you into the wrinkly chamber in no time, <a class="decoration-dotted" id="note-47" href="#note-47" title="I tried to be careful with the language, since this story isn't supposed to be sexual. Maybe it didn't come across too well in the final version (and I'd have to spend way too much time perfecting these descriptions), but I wanted to convey that Sam's gratification is solely from the vore itself, enjoying the hefty prey in his guts and what-not." data-tooltip>earning you a quaking stream of playful growls. Of course, he seems to love having you in there... The beast continues to make joyful moans that sound so alien, from animals you don't recognize.</a><sup>47</sup>
|
||||
|
||||
Still, with newfound resolve, you prepare yourself for this final trial. It's much easier and quicker to make progress in this wide and linear section of his gastrointestinal system. The bigger folds roll around you as your curled body trudges through each corner. It definitely smells in here too, but your nose is too familiar with it to be bothered.
|
||||
|
||||
It's still hard to believe that you went through all of this. That you took a meal's route, forcefully balled-up this whole time; that you managed to use what little and precious resources were at disposal; and, <a class="decoration-dotted" id="note-48" href="#note-48" title="Another instance where I'm not too happy with the language I finally settled with. But balancing the unwillingness and non-fatal aspects was hard enough, and I didn't want this story to never be released, so at one point I settled with 'good enough'." data-tooltip>most of all, that you survived</a><sup>48</sup>. But it's too early to celebrate.
|
||||
|
||||
Your toes brush against yet another sphincter, one with much thicker muscles than the others. As your weight is forced against it, the beast pants and heaves his belly, flipping you around. From the way your surroundings are inclined, he's likely getting into position to push you out.
|
||||
|
||||
He strains himself and pants, putting a lot of effort into passing you. NOW he cares about getting you out of his body. Still, beggars can't be choosers, and you fidget to try and push your toes through the anal barrier.
|
||||
|
||||
Finally, your feet slip out, and the creature lets out a soft whine. You can feel a slight draft – it's so cold compared to these innards –, but it's such a relief to finally move your toes and not brush against another one of his walls! But your extremities are still covered in slime, and it follows them to the outer world. It dreadfully dawns on you that the creature is still standing on your mattress.
|
||||
|
||||
"Not on the bed!" You shout out panickedly. "Not on the– A-Ack...!"
|
||||
|
||||
But it's too late, and the creature doesn't bother to readjust himself. <a class="decoration-dotted" id="note-49" href="#note-49" title="Again, tried to avoid too many descriptions of anal bits, since there's no sexual component to it, simply the hostility of being forced all the way through a pred's guts." data-tooltip>He strains harder, forcing your face against the rectal wall while the bowel compresses you outward.</a><sup>49</sup> More of your ankles and butt get freed, but his anus continues to grip you. Your upper half can barely wriggle in his clenching tunnel, meaning your freedom is subject to his pace.
|
||||
|
||||
These seconds feel like an eternity, slowly feeling your grimy body being deposited onto the soft bed that you'd just cleaned. In a few more moments, your bent legs fully slip out, and they slowly stretch once they find more space. Your knees and muscles feel so sore and weak. The abuse they had to endure finally catches up once they are allowed to move. But they are intact, like the rest of you – and frankly, you can't really ask for more right now.
|
||||
|
||||
Your chest feels lighter once it reaches the other side, and his pucker quickly releases your shoulders as well, growing less tight around the smaller girth of your neck. Only your head remains to be released. You can't wait to get some fresh air, but your arms feel too much like jelly to push against the creature's hinds and free yourself immediately.
|
||||
|
||||
One more squeeze from the feral is all that remains, and with the last of his straining, your head slumps onto the bed along with the enveloping slime. Still mostly curled up in a ball, you sputter the sluices coating your lips and the rest of your body.
|
||||
|
||||
<a class="decoration-dotted" id="note-50" href="#note-50" title="A lot of the editing went into the second half, and a sizable chunk of it focused on the last part of the full tour. Editing in this case mostly consisted moving sentences around, cutting on some superficial descriptions, and reorganizing the break points of sentences into different paragraphs, clustering related stuff in tidy blocks. If that sounds tedious, it's because it is. But I think it's an important process. Not only to make sure your story is understandable on a full pass or has few typos, but that it's paced properly." data-tooltip>The morning light strains your eyes, after getting used to the complete darkness of your captor's insides. The foul smell with the slight hint of lime is perceptible against the fresh air of your room. You shiver as his enveloping warmth quickly dissipates once exposed to the elements.</a><sup>50</sup>
|
||||
|
||||
You're finally free, but your body and mind are both too weak after the ordeal. The better part of tonight has been spent inside of this voracious creature, writhing about instead of getting some much needed rest. Far from a pleasant journey, you wish that you could erase it from your head – but for better or worse, you are alive and free from the beast's clutches.
|
||||
|
||||
But the fluffy perpetrator is still standing on the bed with you. Carefully avoiding any stains from getting to his fur, he jumps off and brings his metallic face close to yours. You can only stare, with anger and scorn, at the rectangular surface. He has no eyes, but <a class="decoration-dotted" id="note-51" href="#note-51" title="It's subtle, but I wanted to give the PoV a bit of an arc. After having to endure all of these events, they can't help but feel contempt for Sam - and that feeling is only strengthened by his alien appearance." data-tooltip>this...thing</a><sup>51</sup> is clearly looking right back at you.
|
||||
|
||||
Then, the briefcase starts to split again, forcing you to realize that the feral still has razor-sharp teeth, a prehensile tongue, and an empty stomach – and you can barely move as he approaches.
|
||||
|
||||
"N-No," you beg hoarsely. "No more..."
|
||||
|
||||
With your spent energy, you can only muster to lift your fingers. The beast's maw follows them, watching the greenish ooze between the digits. Then, it happily licks your hand and purrs, replacing slime with saliva in a lacking attempting at cleaning them.
|
||||
|
||||
<a class="decoration-dotted" id="note-52" href="#note-52" title="A huge hint into Sam's motivations, to complement the other ones peppered about the story! This pretty much confirms that Sam was only having fun, and he had no intention of harming you. But it does leave the reader with contradictory views of both Sam's actions vs. intentions (he wants to play, but he is a bit of a bully by forcing his prey to swallow them down), and the PoV's inner thoughts (the narration sides with them, but their misconceptions are constantly challenged). It's something I purposefully had in mind when writing this story, in order to add some depth and challenge both myself when writing and the reader when interpreting it." data-tooltip>Satisfied with his little show of affection, his tongue retracts into his maw. He closes his jaws, hiding the terrifying features within. As if nothing had happened, he heads to the half-open door, tail wagging on his back, and leaps out into the world, leaving you with even more questions.</a><sup>52</sup>
|
||||
|
||||
You simply stare blankly, fully aware that you're a filthy, aching, sorry mess that smells like canine guts. As the sun shines brightly outside, you collapse against the slimy pool on your bed, <a class="decoration-dotted" id="note-53" href="#note-53" title="Kind of an implicit joke that, whatever the PoV's alarm was for, they are gonna miss it. Maybe work or something? I did want to leave it ambiguous as well, and the fact that it's not explicitly mentioned plays into it. It also plays into the narration reflecting the PoV's internal monologue - they are too exhausted to even worry about that!" data-tooltip>before dozing off for some extremely overdue sleep.</a><sup>53</sup>
|
282
src/content/blog/crossing-over-postmortem.md
Normal file
|
@ -0,0 +1,282 @@
|
|||
---
|
||||
title: Jamming Over
|
||||
pubDate: 2024-03-26
|
||||
isDraft: true
|
||||
isAgeRestricted: true
|
||||
authors: bad-manners
|
||||
# thumbnail: /src/assets/thumbnails/story_thumbnail.png
|
||||
description: |
|
||||
Postmortem about my first vore game, [Crossing Over](/games/crossing-over) – albeit more of an assortment of random thoughts. **Spoilers for Crossing Over ahead!**
|
||||
tags: []
|
||||
relatedGames:
|
||||
- crossing-over
|
||||
---
|
||||
|
||||
A.K.A. that time I made a game in a month.
|
||||
|
||||
_(it really wasn't that long ago...)_
|
||||
|
||||
![Banner art for Crossing Over, featuring four screenshots from the game, underneath a card with the game's title and the tagline "a visual novel about death, fishing, and vore".](../../assets/images/crossing_over/banner.png)
|
||||
|
||||
## Part 1: Preparations
|
||||
|
||||
It all started with an idea for a story. One of many that I had before it got shelved, hoping to one day turn it into an actual story when I had the chance. The initial idea was for an 'endo soul vore' scenario, which I described to a friend (hi Dee!) back in August '23. Here it is reproduced verbatim:
|
||||
|
||||
> Me: I had an idea a long while ago you may like, but I don't think it's something I'd ever feel like sitting down to write
|
||||
|
||||
> Me: Some sort of soul ferry God or spirit that brings souls to the Underworld or something, one of the souls they meet just decides to hang out with them and join them on their job instead of finishing the crossing. They like each other very much but of course the soul can't stay away from the Underworld for too long or it'd dissipate. And if they go to the Underworld they won't see each other again. So instead the spirit/God decides to perma endo them so they can be together forever
|
||||
|
||||
> Friend: oh that's so lovely
|
||||
|
||||
> Me: Could have the soul describing how lonely their life was before they passed away too, regretting that they didn't feel like they'd accomplished anything. And a sprinkle of thoughts on life and death too
|
||||
|
||||
> Me: The soul ferry person would of course be lonely too, meeting new people only to guide them to the Underworld without ever seeing them again, but they can't just shirk from their job
|
||||
|
||||
> Me: Just a fun idea, probably the closest to soul vore I'd ever write lol
|
||||
|
||||
> Friend: i would love to see it
|
||||
|
||||
Of course, I never really went anywhere with it until January '24, when I started to work on a draft for the story. Back then, the story was called "Soul Blues", with the soul character initially being called Fleet – y'know, as in "life is fleeting". By then, I had already worked out a few more themes that I wanted for my story: the soul ferrier had ferried hundreds of thousands of souls before, and would try to learn more about that soul; and the dead person would initially struggle to open up, before reminiscing on their life, and they'd be more ready to accept their death until the moment they fell for the soul ferrier (that part was heavily adjusted in Crossing Over). Still, the project didn't really reel me in, as I felt that I couldn't really articulate the thoughts floating in my head into my usual type of story.
|
||||
|
||||
At the same time, I had been following somewhat closely the starting date for eevee's Strawberry Jam. I'd heard about these jams before I had even begun to post content as Bad Manners, but had never engaged with them before. Still, making a game sounded as fun to me as it does to anyone, and doing it in a low-pressure, kink-positive environment seemed like the best opportunity to try my hand at it. Thankfully, I had a few things that would favor my decision to eventually join it:
|
||||
|
||||
1. An idea. As I've said, the basis of what would eventually become Crossing Over had been floating in my mind for months. While it remained mostly untouched during that period, I still ruminated on other thoughts, like nihilism and depression. I was just starting on some new medication after a rough period of my life, too, so I felt that there was something that I could use it, as a way to put these thoughts in order – perhaps even "console myself" and work through these emotions, accepting them instead of ignoring them.
|
||||
2. Motivation. I had a boost in it not only from getting psychiatric treatment, but also from learning a bunch of stuff. I've been getting better at writing in the two years I've been writing (or I'd like to believe so!), and I'd just started learning Blender earlier in January '24. Plus, I had the desire to try out Godot, learning the basics just two days before the game jam started (it helped that I had a heavy background in programming). As long as I could manage the scope well, I felt that I could deliver a finished project in time.
|
||||
3. Time and energy. Of course, I don't think I would be able to do this project if I still worked full- or even part-time. Despite being unemployed (mostly due to personal reasons), I'm lucky to still have resources to maintain myself for a while. Plus, having just finished two big commissions to my clients' great satisfaction, and then just launched my SubscribeStar, I was feeling more confident than usual that I could pull off such a monumental task.
|
||||
|
||||
Despite all of this, I was still unsure if I should really join, feeling that it'd be a waste of time or that I would deliver something embarrassing. But my friend Hans really encouraged me to move forward, and so, I decided to join Strawberry Jam 8.
|
||||
|
||||
Until February rolled around, I refrained from investing too heavily into the conceptualization of my game – only deciding that I would make a visual novel. I spent most of those few days getting familiar with Godot as well as Blender and MuseScore, since I knew that I wanted to make my own soundtrack using their open high-quality sound library. Still, I was committed from the start to the soul vore story idea, and had some vague notions that it would help with the relatively short time frame:
|
||||
|
||||
- There would be only one anthro character to model and rig, a process that I predicted would take most of my time. The soul character would be much simpler to work with.
|
||||
- I wanted to use a pixel art aesthetic, [using a free plugin I had learned about](https://www.youtube.com/watch?v=vzIVn3G1Z2U). After a month of messing with Blender and learning the basics, I knew that I could create the 3D assets myself and make them more visually appealing than if I tried to draw them. I thought that a toonish style would let me "get away" with the beginner quality models and animations – as in, it'd look better given my current skillset.
|
||||
- With the setting of a soul ferrier on a boat, the environment would be simple (all of it would happen on a boat, with a lot of dialogue between two characters). Writing dialogue has always been easier for me than writing a full descriptive novel with narration, so tackling it as if it were a stage-play seemed not only easier, but it meant that I could focus on my strong parts when writing.
|
||||
|
||||
With all of that in mind – and the nagging feeling that I wouldn't be able to complete the project by the end of February '24 –, I patiently waited for the first day to start working...
|
||||
|
||||
## Part 2: Organization
|
||||
|
||||
Of course, there was a lot of work to do if I was going to make a visual novel all on my own. This included writing, programming, composing soundtracks, art direction and creation (which included 3D assets – from modeling to rigging to animating to rendering –, UI elements, sound effects), creating concept art, play-testing, and so on. In a way, that was a lot of juggle around, as I often wandered between different tasks throughout the project – but that also helped, too. As I had just started on new medication which was helping me with my anxiety, I could focus a lot on each task – and when I couldn't work on a specific one, there were others waiting for me. There wasn't any time to waste! As the month went on, it got easier to switch between multiple tasks, and spend more time working than procrastinating...
|
||||
|
||||
But let's not get ahead of ourselves. First of all, I knew that I had to sit down and decide on an artstyle. I was set on the pixel/cell-shaded visual part, so consequently, I decided to commit to a 3:4 resolution for my game – more specifically, 800x600. Despite messing around with Blender, I knew from the get-go that there wouldn't be any time to get familiar with the 3D side of the Godot engine, and all the assets would have to be rendered into 2D and imported as images, so a low resolution seemed like a perfect fit. However, as I eventually learned throughout the process, 800x600 is still quite large – as well as incompatible with modern resolutions, since it doesn't scale too well to 1080p/2K/4K...and still generates somewhat large PNGs. In retrospect, I'd have gone with a smaller resolution, while still keeping the 3:4 aspect ratio for its unique aesthetic. Perhaps 720x540...? Still, I stuck with it.
|
||||
|
||||
Similarly, I began thinking about what the game would actually be. I wanted a visual novel, sure, but there are still multiple ways to go about it. I thought it'd be a waste to make it completely linear, though. I knew that, at the very least, it should make the choices of the player impactful. I considered giving it multiple endings – one with vore, and one where the soul actually goes to the hereafter –, but gave up on that, as I didn't want to leave the vore part out of the final experience. Eventually, I settled on less impactful dialogue choices.
|
||||
|
||||
I also wanted to break the sameness of the visual novel part with some sort of interactive gameplay. At first, these would be puzzles, and I had the idea for a Genius minigame (repeat the commands), or a Wordle-like minigame (guess the words), in the same way as you would in the Danganronpa series. ...Admittedly, it took me too long to eventually scrap these ideas and go with only a fishing minigame. Which also tied MUCH better into the setting of the game, as the entire story takes place on a boat.
|
||||
|
||||
Now that I had a better notion of what I wanted to make for the next month, it was time to do some extremely crucial, yet invisible work...
|
||||
|
||||
## Part 3: Research and Concept Art
|
||||
|
||||
Research is often one of my favorite parts of writing a story, and this project wasn't any different. I knew what kind of vibe I wanted for the game – some emphasis on depression, with a general uplifting message that life is worth living, despite its inherent lack of meaning. Still, I wanted to see how that kind of message has been used in other places, hoping to get more inspiration for which direction I wanted to take it.
|
||||
|
||||
And aside from reading a lot on Wikipedia about how different cultures see the [afterlife](https://en.wikipedia.org/wiki/Hereafter) (such as the distinction in Islam of dunya, the material world, and [akhirah](https://en.wikipedia.org/wiki/Akhirah), the immaterial world), I also looked at some other media. One of these was [Spiritfarer](https://en.wikipedia.org/wiki/Spiritfarer), which I haven't played but know the gist of. Admittedly, my research into this game was mostly to make sure that my game wouldn't come across as a copy of it... Thankfully, it seemed to have its own approach to the Greek myth of Charon, and a completely different representation of the Underworld, while still sharing some positive messages about the acceptance of death. (I don't wanna spoil it, tho)
|
||||
|
||||
In terms of themes, I put a lot of research into [borderline personality disorder](https://en.wikipedia.org/wiki/Borderline_personality_disorder) – its origins and symptoms –, reflecting my own self-deprecating feelings. I haven't been formally diagnosed, but I do see a lot of myself in it – and writing/researching helped me understand those feelings better, even if I might end up not having this specific disorder. But a much heavier influence into what would become Crossing Over was a children's book called [Duck, Death and the Tulip](https://en.wikipedia.org/wiki/Duck%2C_Death_and_the_Tulip). It's a short and lovely book, and it made me cry on my first read. It really struck me with its message of death's inevitability and uncertainty, a feeling that my game certainly doesn't do enough justice. I've even incorporated duck wings into the boat's design, as an homage to it. Aside from its bittersweet message,
|
||||
|
||||
Speaking of, I did work on some concept art! Including the two characters, the boat, and the environment. The quality of those drawings was terrible (further convincing me to make all the graphics in 3D instead), but it really helped with deciding how to make each element look. I knew that I wanted the environment to be simple, like the inside of a cave, and give the river a striking but single color – the cave portion of Undertale, [Waterfall](https://undertale.fandom.com/wiki/Waterfall), was a strong influence on its look, especially the room with dark floor/walls and cyan water. And the boat, named after the aforementioned "akhirah", was made to look vaguely similar to ancient rowboats used by the Babylonians.
|
||||
|
||||
<figure>
|
||||
|
||||
![Photograph of a page, with several drawings of an anthropomorphic jackal named Marco, wearing a large coat, a mask, and ankle cuffs. There's also a drawing of a boat in the corner.](../../assets/images/crossing_over/marco_concept_art.jpg)
|
||||
|
||||
<figcaption class="text-center">Concept art of Marco and Akhirah. Feel free to judge my awful drawing skills...</figcaption>
|
||||
</figure>
|
||||
|
||||
<figure>
|
||||
|
||||
![The character Marco on his boat over a pink river, leaning down towards the camera with an open hand to the side.](../../assets/images/crossing_over/marco_first_appearance.png)
|
||||
|
||||
<figcaption class="text-center">Marco's (and Akhirah's) first appearance in the game.</figcaption>
|
||||
</figure>
|
||||
|
||||
Marco's design was mostly inspired by that of [Anubis](https://en.wikipedia.org/wiki/Anubis), the Ancient Egyptian god of the underworld, with his dark fur and very long ears. The golden cuffs that he wears are also a reference to him. But I wanted to incorporate other cultural elements associated with death in his design – the skull-like mask that he wears, and his large coat, are somewhat modern interpretations of the grim reaper (with its skeletal body and dark cloak) – but also because I thought muzzle fangs and trenchcoats look cool, lol! When he eventually takes off his mask (which I had planned to be late in the game, when the soul eventually grows weak), he would reveal his green eyes. I had originally intended for the eyes of the mask to glow green as well, but eventually I settled on the red ones, which I thought ended up looking much better. When it came to modeling his head, it took 4 different tries until I was satisfied! As for the name "Marco", he was named after someone from my past, someone very smart and clever and kind – the kind of person one should strive to be, I feel like.
|
||||
|
||||
<figure>
|
||||
|
||||
![Photograph of a page, with several drawings of a blobby and oval character named Bard. There are also a few framed drawings of different angles of scenes taking place on a boat.](../../assets/images/crossing_over/bard_concept_art.jpg)
|
||||
|
||||
<figcaption class="text-center">Concept art of Bard and some screens. I don't think his original look is physically possible...</figcaption>
|
||||
</figure>
|
||||
|
||||
For Bard's design, I had a couple of inspirations – the main one being the visual novel [Ghost Trick](https://en.wikipedia.org/wiki/Ghost_Trick%3A_Phantom_Detective), where the protagonist is also a cyan soul (or a ghost, rather). In that game, ghosts look like flaming balls, and for my take on what a soul would look like, I wanted it to feel more droopy, like something gooey that is slowly dripping and disappearing. As I sat down to draw the concept art, I decided to take some inspiration from the look of [a black hole with an accretion disk surrounding it](https://en.wikipedia.org/wiki/Accretion_disk); a symbol that represents how, inevitably, everything will come to an end. Although I flipped that image on its head (and quite literally too!), giving the core a white appearance instead of black. I played a bit with an upper and lower part surrounding that core, making the top one look inside-out (with the use of Blender's "backface culling") to give it a more ghostly look. The name "Bard" has a few meanings – aside from being non-binary (like the character in my mind), it also represents a bard who tells tales, as well as the association with the word "burden" that Bard themself carries a lot of. I knew that I wanted their soul to be very geometric, and I used a few shapes like ovals, sine waves, and circles to create their unique look.
|
||||
|
||||
<figure>
|
||||
|
||||
![A cyan orb, surrounded on the top by an inside-out dark blue oval, and on the bottom by cyan ribbon-like strands.](../../assets/images/crossing_over/bard.png)
|
||||
|
||||
<figcaption class="text-center">Bard's final look.</figcaption>
|
||||
</figure>
|
||||
|
||||
Both characters underwent several iterations until I was satisfied with them – in fact, I changed them yet again when making the minute-long animation, near the end of the project! Marco especially took most of the work for obvious reasons, but thankfully my friend Hans gave me good pointers on modeling them.
|
||||
|
||||
Actually, now that we're on the subject of models, I should probably change sections...
|
||||
|
||||
## Part 4: Modeling
|
||||
|
||||
There. Still haven't forgotten how to make some text bigger than other text.
|
||||
|
||||
I guess I'll start by talking about something Blender-related that isn't exactly modeling: shading. It simply means selecting the materials for all the different elements – colors, textures, etc. Like I've said back in Part 1, I wanted a pixel art/cellshaded artstyle. And to commit to it even further, I looked for palettes to use for said shading. Limiting the choices helped with making things have a unified identity, as well as stick out from the background and other objects. The surfaces are all made of a similar base material, which creates cells based on the light level and then applies a dithering effect between them (i.e. some pixels of each color blend into each other, between two different cells, as old games with limited palettes did to make smooth transitions). They also had different colors under the two sets of lights: one being "white" for the default look, and the other being "blue" from Bard's glow. This helps with the illusion of the soul actually having its own light. On top of that, I also added an outline to most materials, based off the dark color of the material. This kind of breaks the convention for pixel art, since I'm relying on how Blender generates the alpha for those outlines, but it certainly makes objects much more readable. For each outline, I had to handpick values for the thickness and color – sometimes having to deviate from the palette to do so.
|
||||
|
||||
<figure>
|
||||
|
||||
![A briefcase filled with gold bars.](../../assets/images/crossing_over/briefcase.png)
|
||||
|
||||
<figcaption class="text-center">A gold-filled briefcase, one of the many items that you fish in Crossing Over. Notice the dark yellow outline around the gold bars, and the dark brown one around the light brown bits.</figcaption>
|
||||
</figure>
|
||||
|
||||
The individual items that you fish were straightforward to model, using some low poly techniques, and then applying a subdivision surface modifier (to add more geometry and make shapes more curvy, therefore more realistic). For the fishing items themselves, the modeling was done late into development (once I'd started work on writing, which we'll get to in a later part) – so most of my early modeling was into the two characters and the boat.
|
||||
|
||||
Speaking of, the "wake" effect that you see sometimes while Akhirah is moving (i.e. the foams behind the boat) was easily a day-long chore. For starters, I couldn't remember the word "wake" – so I've spent an embarrassing amount of time trying to remember it, having to go back and skim through my YouTube history (looking for a Minecraft video showcasing [a mod that implemented wakes](https://www.curseforge.com/minecraft/mc-mods/wakes), haha!). Anyway, it was an interesting experience to implement them, [going off of a tutorial](https://www.youtube.com/watch?v=14SNmHvVBio) and having to get more acquainted with some intermediate-level topics from Blender such as conditional shading and using noise displacement. They didn't turn out perfectly realistic, but I think they made the somewhat dull pink environment of the river feel more natural, and the end result was well worth the effort. In fact, the boat doesn't ever move around in the river – and when it does look like it's moving, it's just a bunch of tricks with the camera and the wakes!
|
||||
|
||||
<figure>
|
||||
|
||||
![Marco sitting down against the railing at the back of his boat, without a mask and keeping his hands underneath a hovering Bard. Behind him are wakes being made by the boat moving over the water.](../../assets/images/crossing_over/boat_wakes.png)
|
||||
|
||||
<figcaption class="text-center">A late-game shot, displaying multiple light sources and wakes behind the boat.</figcaption>
|
||||
</figure>
|
||||
|
||||
But as I've hinted at before, Marco's model was the hardest challenge. His model is far from perfect – his arms are too short, his feet are just bricks, his thumbs are in unnatural spots, his lips are wonky, and his body often clipped through his trenchcoat –, which is understandable given my inexperience. But most issues I've managed to cover or fix in an image editor. Despite the inexperienced modeling and rigging, he still turned out decent enough to pose around. It definitely taught me a lot about animating, the hard way! At the very least, my amateurish experience was complemented by retro look that I was going for, so I'll take that as a win.
|
||||
|
||||
## Part 5: Music
|
||||
|
||||
Before I even went too far into modeling, there were two aspects that I wanted to nail down early on: the soundtrack, and the interface. I'll touch on the latter in the next part.
|
||||
|
||||
There were two things that I had stuck in my head before February rolled over: the general idea for what the story was going to be about, and a short musical phrase. "A3-A4-G3-E3 / A3-E3-C3-A3". A short and simple sequence in a minor chord, which would serve as the basis for the main leitmotif in the soundtrack. Although I was afraid it would sound too similar to the theme [Hand Covers Bruise](https://www.youtube.com/watch?v=9SBNCYkSceU) in the movie Social Network (although that one actually goes "A4-G3-Bb3 / D3-C3-F2" when transposed, though I only checked way after I'd released my game); regardless, in the end, it was too striking to not use.
|
||||
|
||||
I've had some amateur experience writing music before – but not on the scale of 17+ minutes combined over the course of a month, and not with MuseScore! Still, getting acquainted with the software (by transposing an old composition, a few days before the jam) certainly helped me when it was time to actually put it to the test.
|
||||
|
||||
When it comes to making music, my process focuses more on individual voices (i.e. each note played by each instrument, including duration and pitch) than harmonies, only falling back to chords in a few cases, such as arpeggios or some fancier harmonization. But sometimes, a chord will simply be the tonic key, or tonic + fifth. I feel that too many voices can muddy the melodies, and make it harder to follow the motions – which, sometimes, can be the desired effect! Percussion is not my strong suit, so you may notice throughout the OST how I often resort to simple (but hopefully effective) rhythms.
|
||||
|
||||
<figure>
|
||||
|
||||
![Part of a page containing sheet music, entitled Loose Thoughts, and composed by Bad Manners. The instruments include low trombone, piano, harp, and strings.](../../assets/images/crossing_over/loose_thoughts.png)
|
||||
|
||||
<figcaption class="text-center">The first musical bars that ever were.</figcaption>
|
||||
</figure>
|
||||
|
||||
The very first track I composed was "Loose Thoughts" – a slow song that starts with a piano and plucked strings. It plays in introspective moments, and it helped me define a few other leitmotifs that would be used ad nauseam in the other tracks, as well as the general vibe. I really dig the orchestral vibe that MuseScore's sound library has to offer, and made a lot of use of violins, cellos and contrabasses in other tracks. Over the first week, I also made "Aboard the Akhirah" (the jazzy song that plays in the more "I don't know if I should trust this ferryman!" moments, with an electric bass and a plucked contrabass before the Hammond organ joins in, and an extended sax solo alter on) and "Fulminant" (the more energetic and catchy main menu song, with a violin and simple drums joined by an electric bass and an acoustic guitar). These three songs really helped steer the general vibe I was going for, in terms of the game's atmosphere.
|
||||
|
||||
Then, as progress was made on other parts of the game, and I had a good idea of how the game would be laid out, I added a few more songs that fit each moment. Often building up with the musical phrases I had so far. I created one song for the fishing minigame ("Under the Surface", a silly and almost childish song), another for Bard's traumatic memories ("Scars", a song that personally evokes feelings of grief and desolation that can't be easily consoled away), and another for the intense moment as Marco scrambles to save Bard ("Eruption", an energetic and messy song that relies heavily on GarageBand's automated drums and MIDI orchestra hits to make the moment feel more urgent).
|
||||
|
||||
Compared to other parts of the project, the soundtrack probably went through the least amount of iterations – though I kept listening to them on repeat, nonstop, to make sure I was happy with them, and make sure that they looped correctly in-game. The whole reason why there's a "music room" as a post-game bonus (you can find it in the "Credits" screen!) was that I could ensure the quality of the loop.
|
||||
|
||||
There were a few tracks that went through a somewhat different creative process. The main one is, of course, "Entangled", which plays during the animation. It wasn't made to loop like the others, since I wanted it to play once while Marco vores Bard. Midway through, the song transitions into a tango (pun intended), playing off the somewhat silly 'choreography' that the pair of characters participates in. I knew that I didn't want to use vore sound effects for the scene, and it was the perfect reason to compose a special track for this special moment. I had originally intended to make a storyboard of the animation; but with the song as the background, I didn't even need it – it was as if the song told the whole story by itself, if that makes sense! It was fun to imagine the vore scene, and then sync the actions to the song itself when animating it.
|
||||
|
||||
The second oddball of a track is "Stars". It was supposed to simply be a lighthearted version of "Scars" (I can't help myself with all the puns, sorry), even starting with a glockenspiel like its sibling, and in the same key but major instead of minor. However, as inspiration struck me, and I started adding more and more instruments, it turned into a whole mini-orchestration. The name alludes not only to the other track, but also to Bard's love for space stuff. Emotionally, I relate the song to a little story: at first, the innocence of childhood, which turns into a memory, and the eventual bittersweetness of nostalgia, coming to a resolution which accepts that not only that our recollection of the past is distorted, but that we eventually have to move forward and create new memories. It was by pure chance that this song was made, but then it didn't fit its original purpose of being a happy tune, but something much more grandiose. So I quickly composed the happy "Afloat" – yep, immediately after! It was past 1AM when I was done! "Afloat" ended up as a simple yet cozy song with an acoustic guitar duo. "Stars" ended up being used in the more impactful and uplifting moments at the end of the story.
|
||||
|
||||
There was one song that I had planned but didn't make it. It was supposed to play at the very end, when the game was over – and I even thought of including lyrics, too! Sadly, after composing 9 tracks, I had exhausted all my creativity in terms of songwriting, but I'm still happy with the ones that did make it into the final game.
|
||||
|
||||
## Part 6: UI and programming
|
||||
|
||||
One of the first things I started working on in the project was the textbox. Compared to other game genres, coding a VN is probably simpler – but I still wanted to nail it down, since I wasn't familiar with Godot. Learning the ropes for development with all the Control and 2D nodes would have been a huge challenge if not for the extensive tutorials out there (I relied on [this nearly 12-hour video tutorial](https://www.youtube.com/watch?v=nAh_Kx5Zh5Q) a lot!) – but the basis for the textbox and architecture was done fairly quickly.
|
||||
|
||||
<figure>
|
||||
|
||||
![A crude digital drawing showing multiple components communicating in order to bring functionality to the game, with emphasis on the ones named visual_novel and VNInterpreter.](../../assets/images/crossing_over/architecture_vn.png)
|
||||
|
||||
<figcaption class="text-center">Basic overview of my visual novel architecture. There's a lot that I would have changed in hindsight...</figcaption>
|
||||
</figure>
|
||||
|
||||
Which isn't to say that implementing more and more features was a breeze. By the end of the first week, when I was handling the load/save system (which took a LOT of work to get right), I was seriously considering switching to something other than Godot, just because its pain points can be annoying. Meanwhile, its strengths – a robust animation system, good input handling, and easy export to multiple platforms – incentivized me to stay. After all, I'd joined the game jam mainly as a learning process, and for better or worse, I was definitely learning a lot.
|
||||
|
||||
<figure>
|
||||
|
||||
![Screenshot of a game screen, with a blue text box displaying the name "Bard" and the parenthesized text "Okay, that's enough...". The background is completely gray.](../../assets/images/crossing_over/textbox.png)
|
||||
|
||||
<figcaption class="text-center">One of the earliest implementations of the text box.</figcaption>
|
||||
</figure>
|
||||
|
||||
Still, I managed to stumble my way through some messy logic, often going for quick and messy fixes because of time constraints. With the basic visual novel architecture and load/save system in place, I added more features – a main menu, prompts, autosaving, music support, backgrounds, text log... When it came time to implement the fishing minigame (by that point, I had scrapped the idea for other puzzle-based minigames), it thankfully wasn't too bad to implement.
|
||||
|
||||
I knew from the start that I wanted a game that ran in the browser (although if that proved too cumbersome, I'd happily ditch it and only release a desktop version). And I also wanted it to support a wide range of devices, to be as accessible as it could be – mouse only, keyboard only, touchscreen, controller. This choice influenced how, for example, the fishing minigame works (with two sets of controllers, depending on if you're using a mouse/a touchscreen or a controller/arrow keys). There was originally going to be a launching mechanism for the net, but I opted for the simpler drag-and-drop. Less models to make, less code to write, and less testing to do.
|
||||
|
||||
Getting the vore animation to work was very frustrating, since Godot's support is very barebones and not well optimized. But at that point, there were only a few days left, and I wasn't going to switch to a new engine. The version that eventually was released for the game jam had some stuttering issues – and the eventual 'fix' (simply handling audio and video streams separately) wasn't really ideal, but it was the best I could come up with.
|
||||
|
||||
## Part 7: Writing
|
||||
|
||||
At this point, one might wonder why I took so long to reach the "writing" section of my postmortem, given that: a) this is a narrative-focused game; and b) my main work so far has been vore stories.
|
||||
|
||||
Well, the truth is that I only started working on it quite late into the project... As in, more than halfway into it! Not that I intended to. I've tried to make a few drafts of the story early on, but I was struggling to figure out the vibes I was going for. In fact, it even took a while to come up with the name "Crossing Over"! I settled on it for a couple of reasons – because it represents a journey not only of going to the Hereafter, but also of pushing forward despite feeling chained to the regrets of the past. Plus the word "over" is an anagram for "vore", and that played a small part in it. Lmao
|
||||
|
||||
So instead, I've left the actual writing for late into the project – once I had established a visual and musical identity for the game, to work off of. Normally, I wouldn't recommend pushing back such a pivotal part of development. But I was confident that I could make it in time, given my previous experience. I thought that the script-like format would be easier for me to make than a full-fledged story.
|
||||
|
||||
And my bluff eventually paid off, as I managed to ruminate on the setting and the characters and their arcs, and I was a lot more confident when it was time to put those thoughts into words.
|
||||
|
||||
<figure>
|
||||
|
||||
![Screenshot of a text editor containing the start of the game's script. It shows that there are 14139 words in the document.](../../assets/images/crossing_over/script_word_count.png)
|
||||
|
||||
<figcaption class="text-center">Word count for the script's draft, with ~1000 different lines, and ~13000 words of dialogue.</figcaption>
|
||||
</figure>
|
||||
|
||||
Aside from the unique challenges of writing for a visual novel (perhaps more akin to stage play direction, having to integrate audiovisual aspects into the script between the lines, with the added challenge of doing it with algorithmic precision), I generally followed the same process for regular stories. I wrote an outline first, breaking the story into different parts and deciding what happened in each one – and choosing the differences between each route, adjusting it until I decided to have four different ones.
|
||||
|
||||
The main drive of the story was the character's both slowly deciding to commit to vore, which is implied perma endo. I wanted the vore to be accessible even for people who don't like vore (and given the feedback from non-vorarephiles on how cozy it felt, I think I succeeded in it!). As for themes itself, obviously I wanted to deal with acceptance of death, as well as some reflection on my own experiences of self-doubt, depression, and dealing with trauma. I wanted to reflect on those feelings in a state of relatively emotional stability, where I could have better hindsight and acceptance about the way that I felt. Mainly, I wanted to show people that these feelings (which are almost never articulated out loud) are not unique to them, that others can understand what they are feeling; and that they aren't wrong or broken because they feel this way. That they might feel guilty, but that guilt is misplaced; that they are still deserving of love, no matter how debilitating their self-deprecating thoughts can be.
|
||||
|
||||
My intent was to make Bard relatable (and making them implicitly non-binary, as well as all the people in their life, was part of it), although a bit anxious and entitled. The "fake amnesia" narrative device served as a way to push forward the narrative in small steps, and explore their mental state, even though I think "character with amnesia" is a common cliché in visual novels. I tried to give it my own spin, but I might've just reinforced the trope instead, lol! I tried not to rely too much in Bard's inner thoughts, trying to focus more on "show, don't tell", unless it was inevitable to convey some crucial information of their mental state. The last third of the game (when Bard falls into despair, then jumps into the river, and finally gets vored by Marco) barely features inner thoughts, reflecting some of the distancing that they put between them and Marco, as well as their own feelings. Although it may be hypocritical to boast about trying to "show, not tell", when I didn't have the time to actually make the animation of them jumping into the water...
|
||||
|
||||
Marco, meanwhile, reflected some of my more positive outlooks on life and nihilism. In a way, he was supposed to be an ideal – a model on how to live optimistically, in my opinion. Early on, I struggled with his personality, as he'd often be too confrontational about Bard shutting themselves off to him. I knew that the key to the story would be the blossoming mutual attraction between Bard and Marco (which culminates in the wholesome and mutual vore scene), so in the end, I've settled on making him a smart and curious soul ferrier, who wants to learn as much about mortal life and how it relates to his own thoughts about his unique nature and relationship with death. And, as always, even he shows some insecurity, such as about his looks or his knowledge, and especially his feelings towards Bard.
|
||||
|
||||
Being set in a limbo-like location – between the mortal life and the Hereafter –, one theme that I toyed with is dealing with Bard's and Marco's unique circumstances as a kind of prelude to a second death, and the uncertainties surrounding it. Bard, still not having overcome their physical death, has to come to terms with the unknowability of the Hereafter (or even about being vored); and Marco, fully aware of his fragile nature, also has to deal with the consequences of shirking his duties vs. denying himself from enjoying life, knowing that he doesn't even have a "Hereafter" to look forward to and that his memories will only last for as long as himself. This kind of tension serves as a metaphor for real death, and how each character comes to terms with what meanings (or lack thereof) they can take from life.
|
||||
|
||||
As for the fishing minigame, it was what I eventually settled upon after giving up on making several different puzzles. Both for time constraint reasons (and the Godot engine really helped with all the in-built physics simulation...!), and because I thought it would be more consistent and engaging than a few slow word-based minigames. Frankly, I dunno why it took me so long to settle on a fishing minigame, given that I'd decided from the start that the entire story would be set on a boat! I created a bunch of objects of desire (somewhat inspired by the "treasures" that you steal in Persona 5, a game which itself has a lot of references to psychoanalysis), often for humorous or referential reasons, and tied them all together to the characters – but mostly Bard, in a way that develops not only them but the people they knew (the different "routes", per se). Coming up with these objects was what eventually led me to Bard's love for space (which relates to their body being inspired by black holes, too), tying that love to certain story beats and deepening their character.
|
||||
|
||||
As I've mentioned back in Part 2, I wanted the choices the player makes to be impactful. At first, there were going to be much more complex conditions to reach one of the routes (the cousin one), and slightly different endings. But since most people would only play the game once, I chose to make these routes more homogeneous, so that they are all valid/canon conclusions and players aren't unjustly punished for accidentally picking the "wrong" route. On the other hand, they all deal with different moments and people in Bard's former life – and the extra lore rewards those who replay the game. They all follow a similar structure, with four "forks":
|
||||
|
||||
1. First, Bard relates some possibility of afterlife to someone in their life;
|
||||
2. Then, they share a memory about that person that gives a hint into Bard's mental state (while also hinting at a darker memory);
|
||||
3. There's the reveal of the abusive experience that solidified their insecurities;
|
||||
4. After being rescued, Bard explains how their life was impacted negatively, and Marco finally puts forth his own insight with a message of love.
|
||||
|
||||
(Perhaps I should record a video showcasing the differences between all of the routes...?)
|
||||
|
||||
The "cousin" route was initially intended to be a complex route, but was eventually adjusted as an option on fork 2, as an alternative to the route picked in the first step. It's also somewhat different from the rest – instead of them being an abusive figure, they were a tragic victim that nevertheless reinforces Bard's self-harming tendencies –, thus giving a stronger hint into Bard's own death.
|
||||
|
||||
Most of these narrative decisions came about in the "outline" portion, to decide how each character should act. Their actual personalities came about when I was writing the script's draft, which would eventually become the actual lines in the visual novel. The draft also included how each character reacts ("Marco feels dejected", "Bard blushes", etc.). These emotion prompts were very useful for posing the characters and creating sprites – I knew beforehand if I had to make them look happy, confused, sad, and so on. I made the sprites in one go, before the draft was implemented as a visual novel. Although later on, I had to redo all of the sprites, because I messed up the shaders...oops.
|
||||
|
||||
<figure>
|
||||
|
||||
![Three screenshots of Marco. From left to right: tilting his head and holding a closed fist underneath his chin; facing down and putting his open hand over his chest; partially covering his smiling snout with his open hand while facing forwards.](../../assets/images/crossing_over/marco_sprites.png)
|
||||
|
||||
<figcaption class="text-center">A few of Marco's sprites, respectively named "curious", "clutching chest", and "playful".</figcaption>
|
||||
</figure>
|
||||
|
||||
There's a lot more that I could say about the story, but it's kinda hard. Writing this was emotionally taxing (I was literally sobbing when I finished it). It always gets me when Bard says "Thank you." at the very end, and I dunno why...! Funny enough, it's hard to explain what I'm writing – instead of explaining which emotions I'm trying to rouse, it's much easier to just rouse them in story form. The story was very personal and dear to me – I've added lots of moments that would make me laugh, cry, ache, and reflect. I could write ten times as many words to go over everything that went through my head when I worked on it. And in some ways, I think this is the best that my writing has ever been.
|
||||
|
||||
## Part 8: Final Days
|
||||
|
||||
With the first version of the script done, as well as most music and character sprites, I've spent the final week putting everything together. I've adapted the draft of my script to fit the format that my Godot code expected, while also creating background sprites (stills and animated) and adjusting individual lines as I worked my way scene-by-scene – while also fixing the occasional bug here and there. It was at this point that all the separate things I had really started to feel like a game – and I was proud of it!
|
||||
|
||||
<figure>
|
||||
|
||||
![Marco and Bard sitting in the boat, with two sprites overlaid. Within those, Bard is taken aback and sweating, and Marco is having a hearty laugh. Through the textbox, Marco says: "Haha, of course not! How would a butt plug bite a fishing hook?"](../../assets/images/crossing_over/scene_in_final_game.png)
|
||||
|
||||
<figcaption class="text-center">A scene from the final game; finally putting UI, dialogue, sprites, music, and backgrounds all together.</figcaption>
|
||||
</figure>
|
||||
|
||||
With a single setting, it was easy to make several animated backgrounds and work with interesting angles and poses – a process which certainly helped me get used to Marco's rig, for the greatest challenge to come. I often replayed each chapter up to 12 times (at least 3 times for each "route"...!), to make sure the dialogue was to my liking, and that effects like bouncing sprites or screen flashes/transitions looked proper. I worked on each scene in sequence, only going back to fix the occasional set-up in the lines. It was a slow but satisfying process.
|
||||
|
||||
With about 4 days before the game jam's deadline, I had ~95% of the project done – I had just added support for sound effects, and only the animation remained to be done. My friend Hans had asked to playtest it, and I wasn't sure I would be able to handle the feedback in time. But at his insistence, I decided to let him play it, as well as our friend Desiran (creator of [Wrangler](https://desiran.itch.io/wrangler)). I wasn't sure how useful their feedback would be, but they managed to give some pointers on some dialogue and bugs, which were relatively easy fixes. And I'm glad that they did play it beforehand, because it meant I could fix these issues that I missed before I ended up publishing my game...!
|
||||
|
||||
And the animation was still a big challenge. I had set aside the last 4 days of February '24 to work exclusively on it. I imagined that it would take a long time to get a minute of animation done – especially since I had to rely on it syncing up with the music that I had done ~2 weeks prior, at that point. But it really wasn't as bad as I thought it would be. I honestly had a blast working on it... And I didn't even need a storyboard! After listening to the track over and over, I had a pretty clear idea of how the scene would go in my head. In just two days, I managed to animate everything (and even dedicate some time to make a nice internal shot, hehe), fix the occasional clipping, and render it all. After struggling to deal with the .ogv video encoder, I managed to get it to work relatively well in-game (with a slight hiccup in the audio that would eventually be fixed in a later release).
|
||||
|
||||
Nevertheless, it was done. About 250 hours of work, and I'd done it. I'd finished my first game...!
|
||||
|
||||
## Part 9: ...Then What?
|
||||
|
||||
I was pretty restless after finishing my game. I'd finished it on the 27th (two days before the deadline), but I was too nervous about releasing it, and decided to leave that for the 28th. A part of me felt that I had made something special, something I was really proud of – and another was very scared of how other people would take it.
|
||||
|
||||
But quickly enough, that anxious side of me was proven wrong. People I knew and people I didn't know messaged me, telling me how much they enjoyed my game. How they've never played any vore-themed game like it, or how they've never seen something that represented their emotions so well. Other people who needed a message like this in a tough moment of their lives. They were touched by the story of Marco and Bard, and in turn, I was touched by their comments. That alone made the month-long effort worth it.
|
||||
|
||||
And I got some criticism, too, especially from other jam participants who were rating the other entries like mine. I'm glad that I did, though! While still generally positive, a lot of the things they pointed out were definitely weaknesses in my final project – for example, how Marco's attraction to Bard was a bit unrealistic, or how the fishing net looked out of place compared to everything else –, and I definitely agree with them! It certainly gave me pointers on how to try to make even better content in the future.
|
||||
|
||||
Although now that it's over, I can't help but feel kind of sad. My depression got worse through the first weeks of March '24 – and I felt guilty that I was feeling down after such a self-accomplishment. It's hard to say why. Maybe it's because I can never hope to make something of this magnitude again. Or that all of my regular writing from now on will pale in comparison to Crossing Over. Or that I can't share these accomplishments with my family and non-furry friends. And that now I have to work as hard as I have for the entirety of February to be seen as valuable by others.
|
||||
|
||||
I know it's ironic, given the messages I tried to convey in the game – that one's accomplishments don't make them deserving of love or not. Maybe it's hypocritical of me to feel down, especially given how well it fared in the game jam, ranking at #1 in Narrative and #2 in both Sound and overall. Well, technically it ranked #1, but only because Itch's ranking algorithm heavily penalized [the contender, which was a whole album of goodies](https://itch.io/jam/strawberry-jam-8/rate/2513403). I think it deserved to be the winner of the category, and I'd still be glad to simply get second place!
|
||||
|
||||
Nonetheless, I can't deny that I feel this way. I've been trying to write more stories since I launched my game, but I can't seem to do anything right (other than fixing every bug I could find in Crossing Over). But maybe I just needed to take a break after working so hard, and not judge myself too harshly over it...
|
||||
|
||||
Well, I managed to finish one other thing, at least: this postmortem! And despite this current slump, I still want to make stuff – more stories and, maybe, even more games. I know that these negative feelings will fade from memory, and that I'll remember this project fondly for months and years to come. I want to make more art – not just for myself, but for others. I want to improve my skills, and I want to bring people joy.
|
||||
|
||||
At the end of the day, I would be happy to become even a fraction of who Marco was for Bard in their darkest hour.
|
|
@ -148,91 +148,101 @@ const copyrightedCharacters = z
|
|||
/** A record of the format `{ en: string, tok?: string, ... }`. */
|
||||
const langRecord = z.object({ [DEFAULT_LANG]: z.string() }).and(z.record(lang, z.string()));
|
||||
/** Common attributes for published content (stories + games). */
|
||||
const publishedContent = z.object({
|
||||
// Required parameters
|
||||
title: z.string(),
|
||||
authors: userList,
|
||||
// Required parameters, but optional for drafts (isDraft === true)
|
||||
pubDate: z
|
||||
.date()
|
||||
.transform((date: Date) => new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0))
|
||||
.optional(),
|
||||
description: z.string().trim(),
|
||||
tags: z.array(z.string()),
|
||||
// Optional parameters
|
||||
isDraft: z.boolean().default(false),
|
||||
isAgeRestricted: z.boolean().default(true),
|
||||
relatedStories: z.array(reference("stories")).default([]),
|
||||
relatedGames: z.array(reference("games")).default([]),
|
||||
lang: lang,
|
||||
series: reference("series").optional(),
|
||||
posts: z
|
||||
.object({
|
||||
eka: z.string().transform((link, ctx) => {
|
||||
const { postId } = parseRegex<{ postId: string }>(
|
||||
/^(?:https?:\/\/)(?:www\.)?aryion\.com\/g4\/view\/(?<postId>[1-9]\d*)\/?$/,
|
||||
)(link, ctx);
|
||||
return { link, postId };
|
||||
}),
|
||||
furaffinity: z.string().transform((link, ctx) => {
|
||||
const { postId } = parseRegex<{ postId: string }>(
|
||||
/^(?:https?:\/\/)(?:www\.)?furaffinity\.net\/view\/(?<postId>[1-9]\d*)\/?$/,
|
||||
)(link, ctx);
|
||||
return { link, postId };
|
||||
}),
|
||||
weasyl: z.string().transform((link, ctx) => {
|
||||
const { user, postId } = parseRegex<{ user: string; postId: string }>(
|
||||
/^(?:https?:\/\/)(?:www\.)?weasyl\.com\/~(?<user>[a-zA-Z][a-zA-Z0-9_-]+)\/submissions\/(?<postId>[1-9]\d*(?:\/[a-zA-Z0-9_-]+)?)\/?$/,
|
||||
)(link, ctx);
|
||||
return { link, user, postId };
|
||||
}),
|
||||
inkbunny: z.string().transform((link, ctx) => {
|
||||
const { postId } = parseRegex<{ postId: string }>(
|
||||
/^(?:https?:\/\/)(?:www\.)?inkbunny\.net\/s\/(?<postId>[1-9]\d*)\/?$/,
|
||||
)(link, ctx);
|
||||
return { link, postId };
|
||||
}),
|
||||
sofurry: z.string().transform((link, ctx) => {
|
||||
const { postId } = parseRegex<{ postId: string }>(
|
||||
/^(?:https?:\/\/)www\.sofurry\.com\/view\/(?<postId>[1-9]\d*)\/?$/,
|
||||
)(link, ctx);
|
||||
return { link, postId };
|
||||
}),
|
||||
sofurrybeta: z.string().transform((link, ctx) => {
|
||||
const { postId } = parseRegex<{ postId: string }>(
|
||||
/^(?:https?:\/\/)sofurrybeta\.com\/s\/(?<postId>[a-zA-Z0-9]+)\/?$/,
|
||||
)(link, ctx);
|
||||
return { link, postId };
|
||||
}),
|
||||
itch: z.string().transform((link, ctx) => {
|
||||
const { user, postId } = parseRegex<{ user: string; postId: string }>(
|
||||
/^(?:https?:\/\/)(?<user>[a-z-]+)\.itch\.io\/(?<postId>[a-z0-9_-]+)\/?$/,
|
||||
)(link, ctx);
|
||||
return { link, user, postId };
|
||||
}),
|
||||
twitter: z.string().transform((link, ctx) => {
|
||||
const { user, postId } = parseRegex<{ user: string; postId: string }>(
|
||||
/^(?:https?:\/\/)(?:www\.)?(?:twitter\.com|x\.com)\/(?<user>[a-zA-Z0-9_-]+)\/status\/(?<postId>[1-9]\d*)\/?$/,
|
||||
)(link, ctx);
|
||||
return { link, user, postId };
|
||||
}),
|
||||
bluesky: z.string().transform((link, ctx) => {
|
||||
const { user, postId } = parseRegex<{ user: string; postId: string }>(
|
||||
/^(?:https?:\/\/)bsky\.app\/profile\/(?<user>(?:[a-zA-Z0-9_-]+\.)+[a-z]+)\/post\/(?<postId>[a-z0-9]+)\/?$/,
|
||||
)(link, ctx);
|
||||
return { link, user, postId };
|
||||
}),
|
||||
mastodon: z.string().transform((link, ctx) => {
|
||||
const { instance, user, postId } = parseRegex<{ instance: string; user: string; postId: string }>(
|
||||
/^(?:https?:\/\/)(?<instance>(?:[a-zA-Z0-9_-]+\.)+[a-z]+)\/@(?<user>[a-zA-Z][a-zA-Z0-9_-]+)\/(?<postId>[1-9]\d*)\/?$/,
|
||||
)(link, ctx);
|
||||
return { link, instance, user, postId };
|
||||
}),
|
||||
})
|
||||
.partial()
|
||||
.default({}),
|
||||
blacklistedMastodonComments: z.array(z.string()).default([]),
|
||||
});
|
||||
const publishedContent = z
|
||||
.object({
|
||||
// Required parameters
|
||||
title: z.string(),
|
||||
authors: userList,
|
||||
// Required parameters, but optional for drafts (isDraft === true)
|
||||
pubDate: z
|
||||
.date()
|
||||
.transform((date: Date) => new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0))
|
||||
.optional(),
|
||||
description: z.string().trim(),
|
||||
tags: z.array(z.string()),
|
||||
// Optional parameters
|
||||
isDraft: z.boolean().default(false),
|
||||
isAgeRestricted: z.boolean().default(true),
|
||||
relatedStories: z.array(reference("stories")).default([]),
|
||||
relatedGames: z.array(reference("games")).default([]),
|
||||
relatedBlogPosts: z.array(reference("blog")).default([]),
|
||||
lang: lang,
|
||||
series: reference("series").optional(),
|
||||
posts: z
|
||||
.object({
|
||||
eka: z.string().transform((link, ctx) => {
|
||||
const { postId } = parseRegex<{ postId: string }>(
|
||||
/^(?:https?:\/\/)(?:www\.)?aryion\.com\/g4\/view\/(?<postId>[1-9]\d*)\/?$/,
|
||||
)(link, ctx);
|
||||
return { link, postId };
|
||||
}),
|
||||
furaffinity: z.string().transform((link, ctx) => {
|
||||
const { postId } = parseRegex<{ postId: string }>(
|
||||
/^(?:https?:\/\/)(?:www\.)?furaffinity\.net\/view\/(?<postId>[1-9]\d*)\/?$/,
|
||||
)(link, ctx);
|
||||
return { link, postId };
|
||||
}),
|
||||
weasyl: z.string().transform((link, ctx) => {
|
||||
const { user, postId } = parseRegex<{ user: string; postId: string }>(
|
||||
/^(?:https?:\/\/)(?:www\.)?weasyl\.com\/~(?<user>[a-zA-Z][a-zA-Z0-9_-]+)\/submissions\/(?<postId>[1-9]\d*(?:\/[a-zA-Z0-9_-]+)?)\/?$/,
|
||||
)(link, ctx);
|
||||
return { link, user, postId };
|
||||
}),
|
||||
inkbunny: z.string().transform((link, ctx) => {
|
||||
const { postId } = parseRegex<{ postId: string }>(
|
||||
/^(?:https?:\/\/)(?:www\.)?inkbunny\.net\/s\/(?<postId>[1-9]\d*)\/?$/,
|
||||
)(link, ctx);
|
||||
return { link, postId };
|
||||
}),
|
||||
sofurry: z.string().transform((link, ctx) => {
|
||||
const { postId } = parseRegex<{ postId: string }>(
|
||||
/^(?:https?:\/\/)www\.sofurry\.com\/view\/(?<postId>[1-9]\d*)\/?$/,
|
||||
)(link, ctx);
|
||||
return { link, postId };
|
||||
}),
|
||||
sofurrybeta: z.string().transform((link, ctx) => {
|
||||
const { postId } = parseRegex<{ postId: string }>(
|
||||
/^(?:https?:\/\/)sofurrybeta\.com\/s\/(?<postId>[a-zA-Z0-9]+)\/?$/,
|
||||
)(link, ctx);
|
||||
return { link, postId };
|
||||
}),
|
||||
itch: z.string().transform((link, ctx) => {
|
||||
const { user, postId } = parseRegex<{ user: string; postId: string }>(
|
||||
/^(?:https?:\/\/)(?<user>[a-z-]+)\.itch\.io\/(?<postId>[a-z0-9_-]+)\/?$/,
|
||||
)(link, ctx);
|
||||
return { link, user, postId };
|
||||
}),
|
||||
twitter: z.string().transform((link, ctx) => {
|
||||
const { user, postId } = parseRegex<{ user: string; postId: string }>(
|
||||
/^(?:https?:\/\/)(?:www\.)?(?:twitter\.com|x\.com)\/(?<user>[a-zA-Z0-9_-]+)\/status\/(?<postId>[1-9]\d*)\/?$/,
|
||||
)(link, ctx);
|
||||
return { link, user, postId };
|
||||
}),
|
||||
bluesky: z.string().transform((link, ctx) => {
|
||||
const { user, postId } = parseRegex<{ user: string; postId: string }>(
|
||||
/^(?:https?:\/\/)bsky\.app\/profile\/(?<user>(?:[a-zA-Z0-9_-]+\.)+[a-z]+)\/post\/(?<postId>[a-z0-9]+)\/?$/,
|
||||
)(link, ctx);
|
||||
return { link, user, postId };
|
||||
}),
|
||||
mastodon: z.string().transform((link, ctx) => {
|
||||
const { instance, user, postId } = parseRegex<{ instance: string; user: string; postId: string }>(
|
||||
/^(?:https?:\/\/)(?<instance>(?:[a-zA-Z0-9_-]+\.)+[a-z]+)\/@(?<user>[a-zA-Z][a-zA-Z0-9_-]+)\/(?<postId>[1-9]\d*)\/?$/,
|
||||
)(link, ctx);
|
||||
return { link, instance, user, postId };
|
||||
}),
|
||||
})
|
||||
.partial()
|
||||
.default({}),
|
||||
blacklistedMastodonComments: z.array(z.string()).default([]),
|
||||
})
|
||||
.refine(
|
||||
({ posts, blacklistedMastodonComments }) => !blacklistedMastodonComments.length || posts.mastodon,
|
||||
`Cannot include "blacklistedMastodonComments" without a Mastodon post`,
|
||||
)
|
||||
.refine(({ isDraft, pubDate }) => isDraft || pubDate, `Missing "pubDate" for published content`)
|
||||
.refine(({ isDraft, description }) => isDraft || description, `Missing "description" for published content`)
|
||||
.refine(({ isDraft, pubDate }) => isDraft || pubDate, `Missing "pubDate" for published content`)
|
||||
.refine(({ isDraft, tags }) => isDraft || tags.length, `Missing "tags" for published content`);
|
||||
|
||||
// Types
|
||||
|
||||
|
@ -267,19 +277,12 @@ const storiesCollection = defineCollection({
|
|||
summary: z.string().trim().optional(),
|
||||
})
|
||||
.and(publishedContent)
|
||||
.refine(({ isDraft, description }) => isDraft || description, `Missing "description" for published story`)
|
||||
.refine(({ isDraft, thumbnail }) => isDraft || thumbnail, `Missing "thumbnail" for published story`)
|
||||
.refine(
|
||||
({ isDraft, contentWarning }) => isDraft || contentWarning,
|
||||
`Missing "contentWarning" for published story`,
|
||||
)
|
||||
.refine(({ isDraft, wordCount }) => isDraft || wordCount, `Missing "wordCount" for published story`)
|
||||
.refine(({ isDraft, pubDate }) => isDraft || pubDate, `Missing "pubDate" for published story`)
|
||||
.refine(({ isDraft, thumbnail }) => isDraft || thumbnail, `Missing "thumbnail" for published story`)
|
||||
.refine(({ isDraft, tags }) => isDraft || tags.length, `Missing "tags" for published story`)
|
||||
.refine(
|
||||
({ posts, blacklistedMastodonComments }) => !blacklistedMastodonComments.length || posts.mastodon,
|
||||
`Cannot include "blacklistedMastodonComments" without a Mastodon post`,
|
||||
),
|
||||
.refine(({ isDraft, wordCount }) => isDraft || wordCount, `Missing "wordCount" for published story`),
|
||||
});
|
||||
|
||||
const gamesCollection = defineCollection({
|
||||
|
@ -299,16 +302,9 @@ const gamesCollection = defineCollection({
|
|||
copyrightedCharacters: copyrightedCharacters,
|
||||
})
|
||||
.and(publishedContent)
|
||||
.refine(({ isDraft, description }) => isDraft || description, `Missing "description" for published game`)
|
||||
.refine(({ isDraft, contentWarning }) => isDraft || contentWarning, `Missing "contentWarning" for published game`)
|
||||
.refine(({ isDraft, platforms }) => isDraft || platforms.length, `Missing "platforms" for published game`)
|
||||
.refine(({ isDraft, pubDate }) => isDraft || pubDate, `Missing "pubDate" for published game`)
|
||||
.refine(({ isDraft, thumbnail }) => isDraft || thumbnail, `Missing "thumbnail" for published game`)
|
||||
.refine(({ isDraft, tags }) => isDraft || tags.length, `Missing "tags" for published game`)
|
||||
.refine(
|
||||
({ posts, blacklistedMastodonComments }) => !blacklistedMastodonComments.length || posts.mastodon,
|
||||
`Cannot include "blacklistedMastodonComments" without a Mastodon post`,
|
||||
),
|
||||
.refine(({ isDraft, contentWarning }) => isDraft || contentWarning, `Missing "contentWarning" for published game`)
|
||||
.refine(({ isDraft, platforms }) => isDraft || platforms.length, `Missing "platforms" for published game`),
|
||||
});
|
||||
|
||||
const blogCollection = defineCollection({
|
||||
|
@ -316,24 +312,14 @@ const blogCollection = defineCollection({
|
|||
schema: ({ image }) =>
|
||||
z
|
||||
.object({
|
||||
// Required parameters, but optional for drafts (isDraft === true)
|
||||
thumbnail: image().optional(),
|
||||
// Optional parameters
|
||||
summary: z.string().trim().optional(),
|
||||
thumbnail: image().optional(),
|
||||
thumbnailWidth: z.number().int().optional(),
|
||||
thumbnailHeight: z.number().int().optional(),
|
||||
prev: reference("blog").nullish(),
|
||||
next: reference("blog").nullish(),
|
||||
})
|
||||
.and(publishedContent)
|
||||
.refine(({ isDraft, description }) => isDraft || description, `Missing "description" for published story`)
|
||||
.refine(({ isDraft, pubDate }) => isDraft || pubDate, `Missing "pubDate" for published story`)
|
||||
.refine(({ isDraft, thumbnail }) => isDraft || thumbnail, `Missing "thumbnail" for published story`)
|
||||
.refine(({ isDraft, tags }) => isDraft || tags.length, `Missing "tags" for published story`)
|
||||
.refine(
|
||||
({ posts, blacklistedMastodonComments }) => !blacklistedMastodonComments.length || posts.mastodon,
|
||||
`Cannot include "blacklistedMastodonComments" without a Mastodon post`,
|
||||
),
|
||||
.and(publishedContent),
|
||||
});
|
||||
|
||||
// Data collections
|
||||
|
|
|
@ -40,6 +40,8 @@ tags:
|
|||
- micro prey
|
||||
- soul vore
|
||||
- long-term endo
|
||||
relatedBlogPosts:
|
||||
- crossing-over-postmortem
|
||||
---
|
||||
|
||||
<iframe
|
||||
|
|
|
@ -109,7 +109,7 @@ First of all, you're too big. There's no way it can swallow you whole...right? B
|
|||
|
||||
Unable to do much, you look back at it. The sight of your feet disappearing between the two rows of fangs is terrifying – but you can feel them in his throat. When you struggle, you catch a glimpse of bulges moving in his neck. His back legs are straightened vertically, and his forepaws are low against the ground. This twisted maned wolf almost looks like a playful pup...and the tail wagging wildly behind him seems to confirm it. He seems to be eating you alive not just for food, but for fun.
|
||||
|
||||
Well, you can't say you're sharing in any of that fun. You thrash and grunt helplessly, yet you continue to gradually vanish within the lime green confines of the unnatural canine. The bulge in his neck slowly shifts down to his chest and belly, distending his slim abdomen with your own limbs. The dry, clean appearance of his furred midriff doesn't reflect the sensations around your feet – it's hot, slimy, and tight inside. He shouldn't be eating you... He /can't/ be eating you! You aren't meant to be food!
|
||||
Well, you can't say you're sharing in any of that fun. You thrash and grunt helplessly, yet you continue to gradually vanish within the lime green confines of the unnatural canine. The bulge in his neck slowly shifts down to his chest and belly, distending his slim abdomen with your own limbs. The dry, clean appearance of his furred midriff doesn't reflect the sensations around your feet – it's hot, slimy, and tight inside. He shouldn't be eating you... He _can't_ be eating you! You aren't meant to be food!
|
||||
|
||||
Keeping you down on the ground, he slowly scoots closer, maneuvering his parted jaws around you to claim you. One of your arm slips free from his tongue's grip, and you reach for the first thing you see – your doormat. You grab it and toss it against your assailant, but it misses and flops pathetically to the side.
|
||||
|
||||
|
|
|
@ -38,18 +38,15 @@ All third-party copyrights, trademarks, and attributed characters belong to thei
|
|||
and I'm not affiliated with any of them."""
|
||||
|
||||
[[attributions]]
|
||||
title = "Noto Sans"
|
||||
type = "font"
|
||||
author = "Noto Project Authors"
|
||||
source = "https://github.com/notofonts/latin-greek-cyrillic"
|
||||
license = { name = "SIL Open Font License v1.1", url = "https://opensource.org/license/ofl-1-1" }
|
||||
|
||||
[[attributions]]
|
||||
title = "Noto Serif"
|
||||
type = "font"
|
||||
title = "Noto"
|
||||
type = "fonts"
|
||||
author = "Noto Project Authors"
|
||||
source = "https://github.com/notofonts/latin-greek-cyrillic"
|
||||
license = { name = "SIL Open Font License v1.1", url = "https://opensource.org/license/ofl-1-1" }
|
||||
items = [
|
||||
"Noto Sans",
|
||||
"Noto Serif",
|
||||
]
|
||||
|
||||
[[attributions]]
|
||||
author = "Simple Icons"
|
||||
|
|
|
@ -101,6 +101,9 @@ const UI_STRINGS = {
|
|||
"published_content/related_games": {
|
||||
en: "Related games",
|
||||
},
|
||||
"published_content/related_blog_posts": {
|
||||
en: "Related blog posts",
|
||||
},
|
||||
"published_content/copyright_aria_label": {
|
||||
en: "Copyright",
|
||||
tok: "toki lawa",
|
||||
|
@ -256,6 +259,31 @@ const UI_STRINGS = {
|
|||
"game/article_aria_label": {
|
||||
en: "Game",
|
||||
},
|
||||
//Blog post-specific strings
|
||||
"blog/return_to_blog_posts": {
|
||||
en: "Return to blog posts",
|
||||
},
|
||||
"blog/title_aria_label": {
|
||||
en: "Post title",
|
||||
},
|
||||
"blog/information_aria_label": {
|
||||
en: "Game information",
|
||||
},
|
||||
"blog/previous_post": {
|
||||
en: (title: string) => `Previous: ${title}`,
|
||||
},
|
||||
"blog/previous_post_aria_label": {
|
||||
en: "Previous post",
|
||||
},
|
||||
"blog/next_post": {
|
||||
en: (title: string) => `Next: ${title}`,
|
||||
},
|
||||
"blog/next_post_aria_label": {
|
||||
en: "Next post",
|
||||
},
|
||||
"blog/article_aria_label": {
|
||||
en: "Blog post",
|
||||
},
|
||||
// Copyrighted character-related strings
|
||||
"characters/copyrighted_characters_aria_label": {
|
||||
en: "Copyrighted characters",
|
||||
|
|
71
src/layouts/BlogLayout.astro
Normal file
|
@ -0,0 +1,71 @@
|
|||
---
|
||||
import { type CollectionEntry, getEntry, getEntries } from "astro:content";
|
||||
import PublishedContentLayout from "./PublishedContentLayout.astro";
|
||||
import { t } from "../i18n";
|
||||
import Authors from "../components/Authors.astro";
|
||||
import UserComponent from "../components/UserComponent.astro";
|
||||
import Prose from "../components/Prose.astro";
|
||||
import { Markdown } from "@astropub/md";
|
||||
|
||||
type Props = CollectionEntry<"blog">["data"];
|
||||
|
||||
const { props } = Astro;
|
||||
const prev = props.prev && (await getEntry(props.prev));
|
||||
const next = props.next && (await getEntry(props.next));
|
||||
const series = props.series && (await getEntry(props.series));
|
||||
const authorsList = await getEntries(props.authors);
|
||||
const relatedStories = (await getEntries(props.relatedStories)).filter((story) => !story.data.isDraft);
|
||||
const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft);
|
||||
---
|
||||
|
||||
<PublishedContentLayout
|
||||
publishedContentType="blog post"
|
||||
title={props.title}
|
||||
lang={props.lang}
|
||||
isDraft={props.isDraft}
|
||||
isAgeRestricted={props.isAgeRestricted}
|
||||
pubDate={props.pubDate}
|
||||
tags={props.tags}
|
||||
thumbnail={props.thumbnail}
|
||||
thumbnailWidth={props.thumbnailWidth}
|
||||
thumbnailHeight={props.thumbnailHeight}
|
||||
series={series}
|
||||
prev={prev && !prev.data.isDraft
|
||||
? {
|
||||
link: `/blog/${prev.slug}`,
|
||||
title: t(props.lang, "blog/previous_post", prev.data.title),
|
||||
}
|
||||
: undefined}
|
||||
next={next && !next.data.isDraft
|
||||
? {
|
||||
link: `/blog/${next.slug}`,
|
||||
title: t(props.lang, "blog/next_post", next.data.title),
|
||||
}
|
||||
: undefined}
|
||||
relatedStories={relatedStories}
|
||||
relatedGames={relatedGames}
|
||||
posts={props.posts}
|
||||
labelReturnTo={{ title: t(props.lang, "blog/return_to_blog_posts"), link: "/blog" }}
|
||||
labelPreviousContent={t(props.lang, "blog/previous_post_aria_label")}
|
||||
labelNextContent={t(props.lang, "blog/next_post_aria_label")}
|
||||
labelTitleSection={t(props.lang, "blog/title_aria_label")}
|
||||
labelInformationSection={t(props.lang, "blog/information_aria_label")}
|
||||
labelArticleSection={t(props.lang, "blog/article_aria_label")}
|
||||
>
|
||||
<meta slot="head" property="og:description" content={props.description} />
|
||||
<Fragment slot="section-information">
|
||||
<Authors lang={props.lang}>
|
||||
{authorsList.map((author) => <UserComponent rel="author" class="p-author" user={author} lang={props.lang} />)}
|
||||
</Authors>
|
||||
<section id="description" class="px-2 font-serif" aria-label={t(props.lang, "published_content/description")}>
|
||||
<Prose>
|
||||
<Markdown of={props.description} />
|
||||
</Prose>
|
||||
</section>
|
||||
</Fragment>
|
||||
<Fragment slot="section-article">
|
||||
<Prose>
|
||||
<slot />
|
||||
</Prose>
|
||||
</Fragment>
|
||||
</PublishedContentLayout>
|
|
@ -41,7 +41,7 @@ const isCurrentRoute = (path: string) =>
|
|||
class="flex flex-col bg-stone-200 text-stone-800 md:flex-row dark:bg-stone-800 dark:text-stone-200 print:bg-none"
|
||||
>
|
||||
<nav
|
||||
class="h-card mb-4 flex shrink-0 flex-col items-center border-b-4 border-bm-400 bg-bm-300 pb-10 pt-10 text-center text-stone-900 shadow-xl md:left-0 md:mb-0 md:min-h-screen md:w-72 md:justify-self-stretch md:border-b-0 md:border-r-4 md:pt-20 dark:border-green-950 dark:bg-green-900 dark:text-stone-100 print:bg-none print:shadow-none"
|
||||
class="h-card mb-4 flex shrink-0 flex-col items-center border-b-4 border-bm-400 bg-gradient-to-b from-bm-300 from-95% to-bm-400 pb-10 pr-3 pt-10 text-center text-stone-900 shadow-lg md:left-0 md:mb-0 md:min-h-screen md:w-72 md:justify-self-stretch md:border-b-0 md:border-r-4 md:bg-gradient-to-r md:pt-20 dark:border-green-950 dark:from-green-900 dark:to-green-950 dark:text-stone-100 print:bg-none print:shadow-none"
|
||||
>
|
||||
<img
|
||||
loading="eager"
|
||||
|
@ -166,12 +166,11 @@ const isCurrentRoute = (path: string) =>
|
|||
import tippy from "tippy.js";
|
||||
import "tippy.js/dist/tippy.css";
|
||||
|
||||
const clipboardItems = document.querySelectorAll<HTMLElement>("[data-clipboard]");
|
||||
|
||||
tippy(clipboardItems, {
|
||||
content: (el) => (el as HTMLElement).dataset.clipboard!,
|
||||
const tooltipItems = document.querySelectorAll<HTMLElement>("[title][data-tooltip]");
|
||||
tooltipItems.forEach((el) => el.setAttribute("data-tooltip", el.title));
|
||||
tippy(tooltipItems, {
|
||||
content: (el) => (el as HTMLElement).dataset.tooltip!,
|
||||
theme: "bm",
|
||||
});
|
||||
|
||||
clipboardItems.forEach((el) => el.removeAttribute("title"));
|
||||
tooltipItems.forEach((el) => el.removeAttribute("title"));
|
||||
</script>
|
||||
|
|
|
@ -34,7 +34,7 @@ type Props = {
|
|||
isDraft: boolean;
|
||||
isAgeRestricted: boolean;
|
||||
pubDate?: Date;
|
||||
description: string;
|
||||
description?: string;
|
||||
summary?: string;
|
||||
tags: string[];
|
||||
thumbnail?: ImageMetadata;
|
||||
|
@ -46,10 +46,11 @@ type Props = {
|
|||
next?: RelatedContent;
|
||||
relatedStories?: CollectionEntry<"stories">[];
|
||||
relatedGames?: CollectionEntry<"games">[];
|
||||
relatedBlogPosts?: CollectionEntry<"blog">[];
|
||||
posts: Posts;
|
||||
|
||||
/* Layout attributes */
|
||||
publishedContentType: "story" | "game";
|
||||
publishedContentType: "story" | "game" | "blog post";
|
||||
labelReturnTo: RelatedContent;
|
||||
labelPreviousContent: string;
|
||||
labelNextContent: string;
|
||||
|
@ -67,7 +68,7 @@ const categorizedTags = Object.fromEntries(
|
|||
),
|
||||
),
|
||||
);
|
||||
const description = await qualifyLocalURLsInMarkdown(props.description, props.lang);
|
||||
const description = props.description && (await qualifyLocalURLsInMarkdown(props.description, props.lang));
|
||||
const summary = props.summary && (await qualifyLocalURLsInMarkdown(props.summary, props.lang));
|
||||
const tags = props.tags.map<{ id: string; name: string }>((tag) => {
|
||||
const tagSlug = slug(tag);
|
||||
|
@ -125,7 +126,7 @@ const returnTo = series
|
|||
<IconArrowBack width="1.25rem" height="1.25rem" />
|
||||
</a>
|
||||
<a
|
||||
href="#description"
|
||||
href="#meta"
|
||||
class="text-link my-1 border-l border-stone-300 p-2 dark:border-stone-700"
|
||||
aria-label={t(props.lang, "published_content/go_to_description")}
|
||||
>
|
||||
|
@ -251,15 +252,20 @@ const returnTo = series
|
|||
</time>
|
||||
) : null
|
||||
}
|
||||
<section id="description" class="px-2 font-serif" aria-describedby="title-description">
|
||||
<h2 id="title-description" class="py-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
{t(props.lang, "published_content/description")}
|
||||
</h2>
|
||||
<Prose>
|
||||
<Markdown of={description} />
|
||||
<CopyrightedCharacters copyrightedCharacters={props.copyrightedCharacters} lang={props.lang} />
|
||||
</Prose>
|
||||
</section>
|
||||
<div id="meta"></div>
|
||||
{
|
||||
description ? (
|
||||
<section id="description" class="px-2 font-serif" aria-describedby="title-description">
|
||||
<h2 id="title-description" class="py-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
{t(props.lang, "published_content/description")}
|
||||
</h2>
|
||||
<Prose>
|
||||
<Markdown of={description} />
|
||||
<CopyrightedCharacters copyrightedCharacters={props.copyrightedCharacters} lang={props.lang} />
|
||||
</Prose>
|
||||
</section>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
summary ? (
|
||||
<section id="summary" class="px-2 font-serif" aria-describedby="title-summary">
|
||||
|
@ -323,8 +329,11 @@ const returnTo = series
|
|||
}
|
||||
{
|
||||
props.relatedStories?.length ? (
|
||||
<section id="related" aria-describedby="title-related" class="my-5">
|
||||
<h2 id="title-related" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
<section id="related-stories" aria-describedby="title-related-stories" class="my-5">
|
||||
<h2
|
||||
id="title-related-stories"
|
||||
class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100"
|
||||
>
|
||||
{t(props.lang, "published_content/related_stories")}
|
||||
</h2>
|
||||
<Prose>
|
||||
|
@ -341,8 +350,11 @@ const returnTo = series
|
|||
}
|
||||
{
|
||||
props.relatedGames?.length ? (
|
||||
<section id="related" aria-describedby="title-related" class="my-5">
|
||||
<h2 id="title-related" class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
<section id="related-games" aria-describedby="title-related-games" class="my-5">
|
||||
<h2
|
||||
id="title-related-games"
|
||||
class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100"
|
||||
>
|
||||
{t(props.lang, "published_content/related_games")}
|
||||
</h2>
|
||||
<Prose>
|
||||
|
@ -357,6 +369,27 @@ const returnTo = series
|
|||
</section>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
props.relatedBlogPosts?.length ? (
|
||||
<section id="related-blog-posts" aria-describedby="title-related-blog-posts" class="my-5">
|
||||
<h2
|
||||
id="title-related-blog-posts"
|
||||
class="p-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100"
|
||||
>
|
||||
{t(props.lang, "published_content/related_blog_posts")}
|
||||
</h2>
|
||||
<Prose>
|
||||
<ul>
|
||||
{props.relatedBlogPosts.map((post) => (
|
||||
<li>
|
||||
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Prose>
|
||||
</section>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
tags.length ? (
|
||||
<section id="tags" aria-describedby="title-tags" class="my-5">
|
||||
|
@ -404,4 +437,12 @@ const returnTo = series
|
|||
placement: "bottom",
|
||||
theme: "bm",
|
||||
});
|
||||
|
||||
const tooltipItems = document.querySelectorAll<HTMLElement>("[title][data-tooltip]");
|
||||
tooltipItems.forEach((el) => el.setAttribute("data-tooltip", el.title));
|
||||
tippy(tooltipItems, {
|
||||
content: (el) => (el as HTMLElement).dataset.tooltip!,
|
||||
theme: "bm",
|
||||
});
|
||||
tooltipItems.forEach((el) => el.removeAttribute("title"));
|
||||
</script>
|
||||
|
|
79
src/pages/blog.astro
Normal file
|
@ -0,0 +1,79 @@
|
|||
---
|
||||
import { Image } from "astro:assets";
|
||||
import { getCollection, getEntries, type CollectionEntry } from "astro:content";
|
||||
import GalleryLayout from "../layouts/GalleryLayout.astro";
|
||||
import { t } from "../i18n";
|
||||
import UserComponent from "../components/UserComponent.astro";
|
||||
import { markdownToPlaintext } from "../utils/markdown_to_plaintext";
|
||||
|
||||
type PostWithPubDate = CollectionEntry<"blog"> & { data: { pubDate: Date } };
|
||||
|
||||
const posts = await Promise.all(
|
||||
((await getCollection("blog", (post) => !post.data.isDraft && post.data.pubDate)) as PostWithPubDate[])
|
||||
.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime())
|
||||
.map(async (post) => ({
|
||||
...post,
|
||||
authors: await getEntries(post.data.authors),
|
||||
})),
|
||||
);
|
||||
---
|
||||
|
||||
<GalleryLayout pageTitle="Blog" class="h-feed">
|
||||
<meta slot="head" property="og:description" content="Bad Manners || A game that I've gone and done." />
|
||||
<h1 class="p-name m-2 text-3xl font-semibold text-stone-800 dark:text-stone-100">Blog</h1>
|
||||
<hr class="mb-3 ml-[2px] mt-2 h-[4px] max-w-xs rounded-sm bg-stone-800 dark:bg-stone-100" />
|
||||
<p class="p-summary my-4">Posts on whatever has been rattling in my head as of late.</p>
|
||||
<ul class="my-6 flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal">
|
||||
{
|
||||
posts.map((post, i) => (
|
||||
<li class="h-entry" lang={post.data.lang}>
|
||||
<a
|
||||
class="u-url text-link hover:underline focus:underline"
|
||||
href={`/blog/${post.slug}`}
|
||||
title={markdownToPlaintext(post.data.description)}
|
||||
data-tooltip
|
||||
>
|
||||
{post.data.thumbnail ? (
|
||||
<div class="flex aspect-[630/500] max-w-[288px] justify-center">
|
||||
<Image
|
||||
loading={i < 10 ? "eager" : "lazy"}
|
||||
class="u-photo m-auto"
|
||||
src={post.data.thumbnail}
|
||||
alt={`Thumbnail for ${post.data.title}`}
|
||||
width={288}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div class="max-w-[288px] text-sm">
|
||||
<span class="p-name" aria-label="Title">
|
||||
{post.data.title}
|
||||
</span>
|
||||
<br />
|
||||
<time
|
||||
class="dt-published italic"
|
||||
datetime={post.data.pubDate.toISOString().slice(0, 10)}
|
||||
aria-label="Publish date"
|
||||
>
|
||||
{post.data.pubDate.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
|
||||
</time>
|
||||
</div>
|
||||
</a>
|
||||
<div class="sr-only select-none">
|
||||
<p class="p-category" aria-label="Category">
|
||||
Blog post
|
||||
</p>
|
||||
<p class="p-summary" aria-label="Summary">
|
||||
{post.data.description}
|
||||
</p>
|
||||
<div aria-label="Authors">
|
||||
<span>{post.authors.length == 1 ? "Author:" : "Authors:"}</span>
|
||||
{post.authors.map((author) => (
|
||||
<UserComponent rel="author" class="p-author" user={author} lang={post.data.lang} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</GalleryLayout>
|
29
src/pages/blog/[...slug].astro
Normal file
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
import type { GetStaticPaths } from "astro";
|
||||
import { type CollectionEntry, getCollection } from "astro:content";
|
||||
import { PUBLISH_DRAFTS } from "astro:env/server";
|
||||
import BlogLayout from "../../layouts/BlogLayout.astro";
|
||||
|
||||
type Props = CollectionEntry<"blog">;
|
||||
|
||||
type Params = {
|
||||
slug: CollectionEntry<"blog">["slug"];
|
||||
};
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
const posts = await getCollection("blog");
|
||||
return posts
|
||||
.filter((post) => import.meta.env.DEV || PUBLISH_DRAFTS || !post.data.isDraft)
|
||||
.map((post) => ({
|
||||
params: { slug: post.slug } satisfies Params,
|
||||
props: post satisfies Props,
|
||||
}));
|
||||
};
|
||||
|
||||
const post = Astro.props;
|
||||
const { Content } = await post.render();
|
||||
---
|
||||
|
||||
<BlogLayout {...post.data}>
|
||||
<Content />
|
||||
</BlogLayout>
|
|
@ -106,6 +106,35 @@ async function gameFeedItem(
|
|||
};
|
||||
}
|
||||
|
||||
async function blogFeedItem(
|
||||
site: URL | undefined,
|
||||
data: EntryWithPubDate<"blog">["data"],
|
||||
slug: CollectionEntry<"blog">["slug"],
|
||||
body: string,
|
||||
): Promise<FeedItem> {
|
||||
return {
|
||||
title: `New blog post! "${data.title}"`,
|
||||
pubDate: toNoonUTCDate(data.pubDate),
|
||||
link: `/blog/${slug}`,
|
||||
description: markdownToPlaintext(await qualifyLocalURLsInMarkdown(data.description, data.lang, site)).replaceAll(
|
||||
/[\n ]+/g,
|
||||
" ",
|
||||
),
|
||||
categories: ["blog post"],
|
||||
commentsUrl: data.posts.mastodon?.link,
|
||||
content: sanitizeHtml(
|
||||
`<h1>${data.title}</h1>` +
|
||||
`<p>${t(
|
||||
data.lang,
|
||||
"story/authors",
|
||||
(await getEntries(data.authors)).map((author) => getLinkForUser(author, data.lang)),
|
||||
)}</p>` +
|
||||
`<hr>${await markdown(await qualifyLocalURLsInMarkdown(data.description, data.lang, site))}` +
|
||||
`<hr>${await markdown(body)}`,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export const GET: APIRoute = async ({ site }) => {
|
||||
const stories = (
|
||||
(await getCollection(
|
||||
|
@ -120,6 +149,11 @@ export const GET: APIRoute = async ({ site }) => {
|
|||
)
|
||||
.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime())
|
||||
.slice(0, MAX_ITEMS);
|
||||
const posts = (
|
||||
(await getCollection("blog", (post) => !post.data.isDraft && post.data.pubDate)) as EntryWithPubDate<"blog">[]
|
||||
)
|
||||
.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime())
|
||||
.slice(0, MAX_ITEMS);
|
||||
|
||||
return rss({
|
||||
title: "Gallery | Bad Manners",
|
||||
|
@ -135,6 +169,10 @@ export const GET: APIRoute = async ({ site }) => {
|
|||
date: data.pubDate,
|
||||
fn: () => gameFeedItem(site, data, slug, body),
|
||||
})),
|
||||
posts.map(({ data, slug, body }) => ({
|
||||
date: data.pubDate,
|
||||
fn: () => blogFeedItem(site, data, slug, body),
|
||||
})),
|
||||
]
|
||||
.flat()
|
||||
.sort((a, b) => b.date.getTime() - a.date.getTime())
|
||||
|
|
|
@ -30,7 +30,7 @@ const games = await Promise.all(
|
|||
class="u-url text-link hover:underline focus:underline"
|
||||
href={`/games/${game.slug}`}
|
||||
title={t(game.data.lang, "game/warnings", game.data.platforms, game.data.contentWarning)}
|
||||
data-clipboard={t(game.data.lang, "game/warnings", game.data.platforms, game.data.contentWarning)}
|
||||
data-tooltip
|
||||
>
|
||||
{game.data.thumbnail ? (
|
||||
<div class="flex aspect-[630/500] max-w-[288px] justify-center">
|
||||
|
|
|
@ -5,6 +5,7 @@ import { Image } from "astro:assets";
|
|||
import GalleryLayout from "../layouts/GalleryLayout.astro";
|
||||
import { t, type Lang } from "../i18n";
|
||||
import UserComponent from "../components/UserComponent.astro";
|
||||
import { markdownToPlaintext } from "../utils/markdown_to_plaintext";
|
||||
|
||||
const MAX_ITEMS = 10;
|
||||
|
||||
|
@ -34,6 +35,11 @@ const games = (
|
|||
)
|
||||
.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime())
|
||||
.slice(0, MAX_ITEMS);
|
||||
const posts = (
|
||||
(await getCollection("blog", (post) => !post.data.isDraft && post.data.pubDate)) as EntryWithPubDate<"blog">[]
|
||||
)
|
||||
.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime())
|
||||
.slice(0, MAX_ITEMS);
|
||||
|
||||
const latestItems: LatestItemsEntry[] = await Promise.all(
|
||||
[
|
||||
|
@ -65,6 +71,20 @@ const latestItems: LatestItemsEntry[] = await Promise.all(
|
|||
pubDate: game.data.pubDate,
|
||||
}) satisfies LatestItemsEntry,
|
||||
})),
|
||||
posts.map((post) => ({
|
||||
date: post.data.pubDate,
|
||||
fn: async () =>
|
||||
({
|
||||
type: "Game",
|
||||
thumbnail: post.data.thumbnail,
|
||||
href: `/blog/${post.slug}`,
|
||||
title: post.data.title,
|
||||
authors: await getEntries(post.data.authors),
|
||||
lang: post.data.lang,
|
||||
altText: markdownToPlaintext(post.data.description),
|
||||
pubDate: post.data.pubDate,
|
||||
}) satisfies LatestItemsEntry,
|
||||
})),
|
||||
]
|
||||
.flat()
|
||||
.sort((a, b) => b.date.getTime() - a.date.getTime())
|
||||
|
@ -75,7 +95,7 @@ const latestItems: LatestItemsEntry[] = await Promise.all(
|
|||
|
||||
<GalleryLayout pageTitle="Gallery" class="h-feed">
|
||||
<meta slot="head" property="og:description" content="Bad Manners || Welcome to my gallery!" />
|
||||
<h1 class="p-name m-2 text-3xl font-semibold text-stone-800 dark:text-stone-100">Welcome to my gallery!</h1>
|
||||
<h1 class="p-name m-2 text-3xl font-semibold text-stone-800 dark:text-stone-100">Gallery</h1>
|
||||
<hr class="mb-3 ml-[2px] mt-2 h-[4px] max-w-xs rounded-sm bg-stone-800 dark:bg-stone-100" />
|
||||
<div class="p-summary">
|
||||
<p class="my-4">
|
||||
|
@ -105,7 +125,7 @@ const latestItems: LatestItemsEntry[] = await Promise.all(
|
|||
class="u-url text-link hover:underline focus:underline"
|
||||
href={entry.href}
|
||||
title={entry.altText}
|
||||
data-clipboard={entry.altText}
|
||||
data-tooltip
|
||||
>
|
||||
{entry.thumbnail ? (
|
||||
<div class="flex aspect-square max-w-[192px] justify-center">
|
||||
|
|
|
@ -88,7 +88,7 @@ const totalPages = Math.ceil(page.total / page.size);
|
|||
class="u-url text-link hover:underline focus:underline"
|
||||
href={`/stories/${story.slug}`}
|
||||
title={t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning)}
|
||||
data-clipboard={t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning)}
|
||||
data-tooltip
|
||||
>
|
||||
{story.data.thumbnail ? (
|
||||
<div class="flex aspect-square max-w-[192px] justify-center">
|
||||
|
|
|
@ -102,7 +102,7 @@ if (uncategorizedTagsSet.size > 0) {
|
|||
class="rounded-full bg-bm-300 px-3 py-1 text-sm text-black shadow-sm hover:underline focus:underline dark:bg-bm-600 dark:text-white"
|
||||
href={`/tags/${id}`}
|
||||
title={description}
|
||||
data-clipboard={description}
|
||||
data-tooltip
|
||||
>
|
||||
{name}
|
||||
</a>
|
||||
|
|
|
@ -11,12 +11,12 @@ type MatchGroups =
|
|||
| {
|
||||
text: string;
|
||||
link: string;
|
||||
contentPrefix: "stories" | "games" | "users";
|
||||
contentPrefix: "stories" | "games" | "blog" | "users";
|
||||
slug: string;
|
||||
};
|
||||
|
||||
const LOCAL_URL_REGEX =
|
||||
/\[(?<text>[^\]]*)\]\((?<link>(?:\/(?<contentPrefix>stories|games|users)(?!\/?\)|\/[1-9]\d*\/?\))\/(?<slug>[^\)]+))|(?:\/[^\)]+?))\/?\)/g;
|
||||
/\[(?<text>[^\]]*)\]\((?<link>(?:\/(?<contentPrefix>stories|games|blog|users)(?!\/?\)|\/[1-9]\d*\/?\))\/(?<slug>[^\)]+))|(?:\/[^\)]+?))\/?\)/g;
|
||||
|
||||
const SERIES_URLS_REGEX_PROMISE = getCollection("series").then(
|
||||
(series) => new RegExp(`^(${series.map(({ data }) => data.link.replace(/\/$/, "")).join("|")})\/?$`),
|
||||
|
@ -71,6 +71,16 @@ export async function qualifyLocalURLsInMarkdown(originalText: string, lang: Lan
|
|||
continue;
|
||||
}
|
||||
break;
|
||||
case "blog":
|
||||
const post = await getEntry("blog", slug);
|
||||
if (!post) {
|
||||
throw new Error(`Couldn't find blog post with slug "${slug}"`);
|
||||
}
|
||||
if (typeof website === "string" && post.data.posts[website as PostWebsite]?.link) {
|
||||
replacements.push(`[${text}](${post.data.posts[website as PostWebsite]!.link})`);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case "users":
|
||||
const user = await getEntry("users", slug);
|
||||
if (!user) {
|
||||
|
|
|
@ -7,8 +7,9 @@ export default {
|
|||
content: {
|
||||
files: [
|
||||
"./src/components/**/*.astro",
|
||||
"./src/content/games/**/*.{md,mdx}",
|
||||
"./src/content/stories/**/*.{md,mdx}",
|
||||
"./src/content/games/**/*.{md,mdx}",
|
||||
"./src/content/blog/**/*.{md,mdx}",
|
||||
"./src/layouts/*.astro",
|
||||
"./src/pages/**/*.{astro,ts}",
|
||||
],
|
||||
|
|