let it leave me like a long breath

let it dissipate or fade in the background

(no subject)

Profile

xax: purple-orange {11/3 knotwork star, pointed down (Default)
howling howling howling

Nav

  • Recent Entries
  • Archive
  • Reading
  • Tags
  • Memories
  • Profile

Tags

  • art - 2 uses
  • asteroid garden - 4 uses
  • code - 19 uses
  • demos - 1 use
  • dreams - 5 uses
  • ff7 fangame - 23 uses
  • fic prompts - 13 uses
  • gamedev challenge - 82 uses
  • hell game - 76 uses
  • nanowrimo - 11 uses
  • plants - 9 uses
  • process - 52 uses
  • programming - 51 uses
  • screenshots - 5 uses
  • writing log - 83 uses

May 2025

S M T W T F S
    123
45678 910
1112131415 1617
18192021222324
25262728293031
  • Oct. 18th, 2017
  • xax: purple-orange {11/3 knotwork star, pointed down (Default)
    [personal profile] xax
    Tags:
    • code,
    • hell game,
    • programming
    posted @ 07:56 am

    or, to make the stuff in that last post more explicit and code-backed:

    (you could also think of it as a sequel to this post from a year and a half ago, because there i was talking about an earlier version of the encounter code. as you can see from this post, it's grown since then.)

    so internally an Encounter is this:

    type Encounter i vars mob = BodyGlob -> mob -> [Event mob i vars]

    where i is the scene-indexing type, vars is the local-variables type, and mob is the encounter type. Event itself looks like this:

    data Event e a l
    	= Narration [String]
    	| Narration2 EventReel
    	| Dialog String String [String]
    	| Dialog2 String String EventReel
    	| Dialog3 String EventReel EventReel
    	| ItemModify [Item]
    	| NodeGive TfNode
    	| Choices [(String, a)]
    	| Choices2 [(String, a, Maybe (Cond l))]
    	| Challenge Opponent [[Item]]
    	| FauxChoice String
    	| Jump a
    	| Shop String [Alchemy]
    	| Close
    	| DeactivateSelf
    	| InlineBranch (Cond l) [Event e a l] [Event e a l]
    	| SetFlag (Scope l) SetMod
    	| UnsetFlag (Scope l)
    	| SetEncounterData e
    	| PushEncounterData (Scope l)
    	| LoadAllSubData (Scope l) [String]
    	| PopToSubData (Scope l) [String]
    	| PeekLoadEncounterData (Scope l)
    	| Branch [(Cond l, a)] a
    	deriving (Show)

    and is basically a list of every possible discrete thing that can happen in a game event -- an event can generate text narration, or text in a dialog frame, or give/remove PC items, or give body nodes, or present a list of choices, etc etc etc onto the more abstruse ones like PeekLoadEncounterData, which i don't even remember what it does.

    the type of Encounter means that, on the server side, when generating the text for an encounter, each encounter has access to precisely two things: the pc body state, and the mob encounter state, both at the precise state they were in when the encounter request was sent (i.e., you can't change an encounter midway through and expect the later scene text to respond to that; all reachable encounter paths are generated once, at request time. when i first started coding the encounter system i made some attempts to change that, but as you might imagine that kind of flow analysis isn't super trivial so there's a certain amount of dead code devoted to path flow analysis.)

    more specifically, each game encounter file provides a few things, most notably an instance to Scene:

    class ToJSON mob => Scene i vars mob | i -> mob vars, vars -> i mob where
    	link :: i -> Encounter i vars mob
    	display :: i -> String
    	vardisplay :: vars -> String
    	varread :: String -> Maybe vars
    	varread _ = Nothing

    which has to do with translating all the random haskell code that makes up an encounter into something that can be serialized (into JSON) and sent back to the client. from the bottom up:

    • varread is dead code ostensibly used for reading the client-side encounter variable table, which is sent with the request but (currently) never used
    • vardisplay is to convert local/global variable references (which exist in code in a form like Local HasMet or Global FarmQuestActive) into plaintext so that the client-side encounter variable table has names to index its variables with
    • display is to render the scene index type, which is used because the actual ~scene api~ responds with a big lookup table of scene data, indexed by the scene index type (hence the name), so that also needs to be renderable as plaintext
    • link, which is the big one, and which determines how we look up scene parts by index on the server side. functions of type a -> b are effectively a total lookup table of the form [(a, b)], so, this is a lookup table with i as the key and Encounter i vars mob as the value.

    so for example, the scene instance for the flame demon encounter looks like this:

    instance Scene Index Vars FlameEncounter where
    	link x = case x of
    		OpeningOptions -> openingOptions
    		TalkOpening -> talkOpening
    		Talk s -> talk s
    		ShopPrompt -> shopPrompt
    		Leave -> leave
    		FuckIntro -> fuckIntro
    		JerkOff from -> jerkOff from
    		BlowTop -> blowTop
    		MouthClimax -> mouthClimax
    		ThroatClimax -> throatClimax
    		Blow from -> blow from
    		SitAnal from -> sitAnal from
    		MountAnal from -> mountAnal from
    		CagePlay -> cagePlay
    		YeahFuck from -> yeahFuck from
    		Fist from -> fist from
    		Reject -> reject
    		Bye -> bye
    		Uncurse -> uncurse
    	display = fmap toLower . show
    	vardisplay = fmap toLower . show

    (this is basically the pattern all the scene instances fall into, btw -- a case statement to link scene indexes with similarly-named functions, and then display/vardisplay being basically show.)

    SO if you read that prior post, here is the technical aspect of what i was calling 'subevents': if you have location X that could potentially store NPCs of type A, B, or C, then what in the world does its scene instance look like? specifically, given that an event is gonna return values of type Event mob i vars, and it's not really feasible to edit the encounter code directly for every possible location that might store an NPC, how do we change those values around to basically turn events on one mob/index/vartable into events on a DIFFERENT mob/index/vartable?

    if you know haskell you might already be having a sinking feeling here

    the answer is, of course, that Event mob i vars forms a TRIFUNCTOR, and can be mapped over to convert values that match one Scene instance into values that match a different one. here:

    triMap :: (e -> f) -> (a -> b) -> (l -> m) -> Event e a l -> Event f b m
    triMap ef af lf e = case e of
    	Choices cs -> Choices $ (\(s, a) -> (s, af a)) <$> cs
    	Choices2 cs -> Choices2 $ (\(s, a, mcl) -> (s, af a, (fmap lf) <$> mcl)) <$> cs
    	Jump a -> Jump $ af a
    	InlineBranch cl e e' -> InlineBranch (lf <$> cl) (triMap ef af lf <$> e) (triMap ef af lf <$> e')
    	SetFlag sl m -> SetFlag (lf <$> sl) m
    	UnsetFlag sl -> UnsetFlag (lf <$> sl)
    	SetEncounterData e -> SetEncounterData $ ef e
    	PushEncounterData sl -> PushEncounterData (lf <$> sl)
    	LoadAllSubData sl s -> LoadAllSubData (lf <$> sl) s
    	PopToSubData sl s -> PopToSubData (lf <$> sl) s
    	PeekLoadEncounterData sl -> PeekLoadEncounterData (lf <$> sl)
    	Branch cs a -> Branch ((\(cl, a') -> (lf <$> cl, af a')) <$> cs) (af a)
    
    	Narration s -> Narration s
    	Narration2 e -> Narration2 e
    	Dialog i n s -> Dialog i n s
    	Dialog2 i n e -> Dialog2 i n e
    	Dialog3 i n e -> Dialog3 i n e
    	ItemModify is -> ItemModify is
    	NodeGive tf -> NodeGive tf
    	Challenge o is -> Challenge o is
    	FauxChoice s -> FauxChoice s
    	Shop s as -> Shop s as
    	Close -> Close
    	DeactivateSelf -> DeactivateSelf

    (to the best of my knowledge there's no major haskell library that provides an actual Trifunctor class that i could instance, so i was just like 'hey whatever' and decided to just write the map function as a standalone.)

    so, while every existing scene instance took a form basically matching that example above, these parametric locations have some very different scene instances:

    data CedarTree = CDryad Dryad | CNature NatureSpirit | CUnicorn Unicorn
    	deriving (Eq, Ord, Show, Read)
    
    data Index = DIndex Dryad.Index | UIndex Unicorn.Index | NIndex Nature.Index
    	deriving (Eq, Ord, Show, Read)
    
    data Var = AsDryad Dryad.Vars | AsUnicorn () | AsNature ()
    	deriving (Eq, Ord, Show, Read)
    
    asDryad :: Event Dryad Dryad.Index Dryad.Vars -> Event CedarTree Index Var
    asDryad = triMap CDryad DIndex AsDryad
    
    asNature :: Event NatureSpirit Nature.Index () -> Event CedarTree Index Var
    asNature = triMap CNature NIndex AsNature
    
    asUni :: Event Unicorn Unicorn.Index () -> Event CedarTree Index Var
    asUni = triMap CUnicorn UIndex AsUnicorn
    
    instance Scene Index Var CedarTree where
    	link f = case f of
    		DIndex di -> \b c -> case c of
    			CDryad d -> asDryad <$> link di b d
    			_ -> error "bad cedartree scene"
    		UIndex ui -> \b c -> case c of
    			CUnicorn u -> asUni <$> link ui b u
    			_ -> error "bad cedartree scene"
    		NIndex ni -> \b c -> case c of
    			CNature n -> asNature <$> link ni b n
    			_ -> error "bad cedartree scene"
    	display = fmap toLower . show
    	vardisplay = fmap toLower . show

    amazing

    (also note the use of () here -- generally that's what i use for encounters that don't have any local variables. you might think that using () multiple times in a scene instance violates the vars -> i mob fundep in the Scene definition, and you would be correct. but because haskell doesn't exhaustively check for overlapping instances, and since (generally) each scene is in a separate compilation unit, and since i never plan on trying to uniquely identify a scene by vardisplay (), i think i'm fine.)

    (anyway this is interesting b/c the dryad code has a local variable named Met, that's used to determine if you're revisiting a dryad. in this encounter, rather than Met being set and read to, a completely different variable, AsDryad Met, is set and referred to. since that's ultimately what being able to (tri)map over events means: that it's possible to rewrite index tables and vartables to break the correspondence with one scene while creating a new correspondence with another scene. this doesn't yet work 100% for reasons that i'm unsure about, and i think that might just be "the events i've written are poorly-structured to handle having a nested entry point", but it's mostly working. for example, in the current code, the npcs in the cedar tree location get their descriptions printed twice: once because i have the cedar tree opening narration describe their contained npc before jumping into the npc's scene, and the other... in the contained npc's scene.)

    (the next issue here is just the structural one of jumping into another encounter -- right now once you jump into an encounter you're basically stuck in the subtree until it ends; there's not really any jumping 'out'. but it's entirely POSSIBLE to explicitly change the link index of a subevent. but generally those aren't surfaced, and there's no functional infrastructure to do things like "oh okay run the subevent's description code, and then instead of the usual choices present "look around the base of the tree" and "talk to the dryad"", or "instead of 'leave' in the subevent exiting the event, have it jump back to the main event's choice prompt". i could handle things like that on a case-by-case basis, but it would involve surfacing more subevent code and generally be arbitrary and tangled, so once this is fully working from a code perspective the next issue is to think about what added structure would help with nesting events)

    ANYWAY THOSE ARE SOME TECHNICAL DETAILS, HOPE YOU LIKED THEM

    • Previous Entry
    • Add Memory
    • Share This Entry
    • Next Entry
    • 1 comment
    • Reply
  • Threaded | Top-Level Comments Only
    • snao: (Default)
      [personal profile] snao
      posted @ 04:42 pm (UTC)

      no subject

      It's a bit overwhelming but I hope someone else got more outta it!

      • Link
      • Reply
    • Previous Entry
    • Add Memory
    • Share This Entry
    • Next Entry
    Threaded | Top-Level Comments Only
Page generated Jan. 23rd, 2026 03:51 am
Powered by Dreamwidth Studios

Style Credit

  • Style: (No Theme) for vertical