diff --git a/assets.mdesk b/assets.mdesk index 09113d2..466f5c5 100644 --- a/assets.mdesk +++ b/assets.mdesk @@ -18,6 +18,10 @@ { filepath: "grunt_3.wav", } +@image peace_totem: +{ + filepath: "peace_totem.png", +} @image merchant: { filepath: "copyrighted/merchant.png", diff --git a/assets/drama.mdesk b/assets/drama.mdesk new file mode 100644 index 0000000..00de87c --- /dev/null +++ b/assets/drama.mdesk @@ -0,0 +1,54 @@ +[ + {can_hear: [NPC_Jester, NPC_Bill]}, + {enum: NPC_Jester , dialog: "Hehehe! Quit quant, a peasant from the mortal realm!"}, + {enum: NPC_Bill , dialog: "No...Please! Stay away from me! Who the Hell are you!", thoughts: "I'm kind of mad, and this Jester guy is kind of an asshole"}, + {enum: NPC_Jester , dialog: "Poor Bill, I'm sure your ex-wife says the same..."}, + {enum: NPC_Bill , dialog: "You evil BASTARD!! What is this place???", thoughts: "I hate the Jester because he dissed my ex-wife. And, he's being all vague and condescending. I would punch him if I could"}, + {enum: NPC_Jester , dialog: "I'll leave that for you to find out ;)", action: ACT_causes_testicular_torsion}, + {enum: NPC_Bill , dialog: "AUGGHGHHH!!! THE PAIN IS UNBEARABLE", thoughts: "SCREW THIS JESTER GUY HE FUCKED WITH MY NUTS" }, + {enum: NPC_Jester , dialog: "That's just a taste of what's to come! Bye bye~", action: ACT_undoes_testicular_torsion}, + {can_hear: [NPC_Jester, NPC_TheBlacksmith]}, + {enum: NPC_Jester , dialog: "A fine strong man you are, mister meld!"}, + {enum: NPC_TheBlacksmith, dialog: "I'll take any complement I can get, but you suddenly knowing my name is pretty weird dude.", thoughts: "This Jester looking guy gives me the creeps..."}, + {enum: NPC_Jester , dialog: "A strong man needs strong metal...Some magic magnetic fish could do the trick! They'd pick up all the river iron"}, + {enum: NPC_TheBlacksmith, dialog: "You know what I'll bite, where do I get this fish.", thoughts: "I can't believe business is so poor that I'm stopping to this..."}, + {enum: NPC_Jester , dialog: "Come now, follow me..."}, + {can_hear: [NPC_Jester, NPC_TheBlacksmith, NPC_Edeline]}, + {enum: NPC_Jester , dialog: "Edeline, fine enchantress. This blacksmith has asked for your hand in marriage if you would enchant these fish for him"}, + {enum: NPC_TheBlacksmith, dialog: "Y-yes...", thoughts: "THAT BASTARD! It'd be too awkward to back out now..."}, + {enum: NPC_Edeline , dialog: "Oh my gosh! I leap with joy! I see much joy in our future, Meld.", thoughts: "I really hope he doesn't back out, I'm tired of being the crazy town spinster!"}, + {enum: NPC_Jester , dialog: "The fish!"}, + {enum: NPC_Edeline , dialog: "AH! Of course, here you are then!", thoughts: "I have no idea what these magnetic fish would be good for...", action: "ACT_enchant_fish"}, + {enum: NPC_TheBlacksmith, dialog: "Finally, maybe this will help me replenish my metal reserves", thoughts: "I have no idea where all my metal went. I wonder if somebody stole it"}, + {can_hear: [NPC_Jester, NPC_Blue]}, + {enum: NPC_Blue , dialog: "What the-my rod! These fish broke my fishing rod! Why are they glowing???", thoughts: "I'll be destroyed in the free market if I don't figure this out!"}, + {enum: NPC_Jester , dialog: "Ho-ho! You know, if you want your rod to get done in split, Meld's anvil's got plenty reserves..."}, + {enum: NPC_Blue , dialog: "Are you saying I should steal, betray the trust of Meld, just so my company makes more money? Sold!", thoughts: "Meld's fault for not hiring private security!"}, + {can_hear: [NPC_Jester, NPC_Blue, NPC_TheBlacksmith]}, + {enum: NPC_Blue , dialog: "Hey meld, what's that shiny thing over there?", thoughts: "Time to steal me an iron pole for my iron ROD!"}, + {enum: NPC_TheBlacksmith, dialog: "Hm?", thoughts: "What is it now...."}, + {can_hear: [NPC_Jester, NPC_Blue]}, + {enum: NPC_Blue , dialog: "Hehehe...", thoughts: "EASY!", action: "ACT_steal_iron_pole"}, + {can_hear: [NPC_Jester, NPC_Blue, NPC_TheBlacksmith]}, + {enum: NPC_TheBlacksmith, dialog: "There wasn't anything over there?", thoughts: "What does this guy think he's doing?"}, + {enum: NPC_Blue , dialog: "Don't worry about it man! You have a good one", thoughts: "PEACE OF CAKE!"}, + {can_hear: [NPC_Jester, NPC_Davis]}, + {enum: NPC_Jester , dialog: "You know Davis, I can promise you fortune and power beyond your comprehension if you assist me in my antics. I mean to bring devastation to this town"}, + {enum: NPC_Davis , dialog: "...", speech: "This jester guy reminds me of when I was younger."}, + {enum: NPC_Jester , dialog: "Cat got your tongue?"}, + {enum: NPC_Davis , dialog: "You need friends so badly, mister jester, that you resort to disgusting party tricks and trivial annoyance? I will not allow this.", thoughts: "He can stare me down all he wants, all he'll find is a blank mirror."}, + {enum: NPC_Jester , dialog: "Careful there, diogenes! I've got more knives than you fingers!"}, + {enum: NPC_Davis , dialog: "Petty violence means nothing to me. Can't you see where you end? At nothing, where you began.", thoughts: "I am empty of everything."}, + {enum: NPC_Jester , dialog: "Have it your way! I'll be in touch..."}, + {enum: NPC_Davis , dialog: "If you bring me death, I welcome it. If you bring me victory, I am unperturbed. If you return, I will have your head, no matter the cost.", thoughts: "It really is a nice day out today."}, + + {can_hear: [NPC_Jester, NPC_Red]}, + {enum: NPC_Jester , dialog: "Red, everybody in this town is a capitalist, and you must overthrow them!"}, + {enum: NPC_Red , dialog: "What?? This is an OUTRAGE!", thoughts: "CAPITALIST PIGS!"}, + + {can_hear: [NPC_Jester, NPC_TheKing]}, + {enum: NPC_Jester , dialog: "Silly king, you think you have power? Your townsfolk are sure to revolt..."}, + {enum: NPC_TheKing , dialog: "This is unacceptable...you must tell me more.", thoughts: "If this jester lies I will destroy him."}, + {enum: NPC_Jester , dialog: "Blue and Red are spouting free-will propaganda, tarnishing your position. Meld is disgusted by what your tarriffs impose on him, and Esmerelda fortells of doom and chaos to fall upon your kingdom."}, + {enum: NPC_TheKing , dialog: "This is dire indeed...I will have to think on this. Thank you, Jester, for the warning.", thoughts: "I must remain calm and collected, even in the sightlines of such terrible danger to my kingdom."}, +] diff --git a/assets/new_level.json b/assets/new_level.json index 0752b86..290264b 100644 --- a/assets/new_level.json +++ b/assets/new_level.json @@ -344,12 +344,12 @@ "class":"", "height":32, "id":4, - "name":"TheKing", + "name":"PeaceTotem", "rotation":0, "visible":true, "width":32, - "x":1523.72727272728, - "y":1729.15151515151 + "x":1522.39393939395, + "y":1570.48484848484 }, { "class":"", @@ -365,8 +365,8 @@ "rotation":0, "visible":true, "width":32, - "x":1504.33333333334, - "y":2658 + "x":1601.66666666667, + "y":1647.33333333333 }, { "class":"", @@ -382,8 +382,8 @@ "rotation":0, "visible":true, "width":32, - "x":1540.66666666668, - "y":2094.33333333334 + "x":1383.33333333335, + "y":2095.66666666667 }, { "class":"", @@ -421,8 +421,8 @@ "rotation":0, "visible":true, "width":32, - "x":1271, - "y":2100 + "x":1128.33333333333, + "y":2089.33333333333 }, { "class":"", @@ -439,12 +439,34 @@ "class":"", "height":32, "id":28, + "name":"Jester", + "rotation":0, + "visible":true, + "width":32, + "x":3296.66666666667, + "y":3270.33333333333 + }, + { + "class":"", + "height":32, + "id":29, "name":"Bill", "rotation":0, "visible":true, "width":32, - "x":1523.33333333334, - "y":2551.33333333333 + "x":1511, + "y":2513 + }, + { + "class":"", + "height":32, + "id":30, + "name":"TheKing", + "rotation":0, + "visible":true, + "width":32, + "x":1516, + "y":1720 }], "opacity":1, "type":"objectgroup", @@ -453,7 +475,7 @@ "y":0 }], "nextlayerid":5, - "nextobjectid":29, + "nextobjectid":31, "orientation":"orthogonal", "renderorder":"right-down", "tiledversion":"1.9.2", diff --git a/assets/peace_orb.png b/assets/peace_orb.png new file mode 100644 index 0000000..9d6e4c5 Binary files /dev/null and b/assets/peace_orb.png differ diff --git a/assets/peace_totem.png b/assets/peace_totem.png new file mode 100644 index 0000000..70bd1a9 Binary files /dev/null and b/assets/peace_totem.png differ diff --git a/character_info.h b/character_info.h index 46492f2..ff717a4 100644 --- a/character_info.h +++ b/character_info.h @@ -3,9 +3,9 @@ #include "HandmadeMath.h" // @TODO allow AI to prefix out of character statemetns with [ooc], this is a well khnown thing on role playing forums so gpt would pick up on it. -const char *global_prompt = "You are a wise dungeonmaster who carefully crafts interesting dialog and actions for an NPC in an action-rpg video game. It is critical that you always respond in the format shown below, where you respond like `ACT_action \"This is my response\", even if the player says something vulgar or offensive, as the text is parsed by a program which expects it to look like that. Do not ever refer to yourself as an NPC or show an understanding of the modern world outside the game, always stay in character.\n" +const char *global_prompt = "You are a wise dungeonmaster who carefully crafts interesting dialog and actions for an NPC in an action-rpg video game. It is critical that you always respond in the format shown below, where you respond like `ACT_action \"This is my response\" [This is my internal monologue]`, even if the player says something vulgar or offensive, as the text is parsed by a program which expects it to look like that. Do not ever refer to yourself as an NPC or show an understanding of the modern world outside the game, always stay in character.\n" "Actions which have () after them take an argument, which somes from some information in the prompt. For example, ACT_give_item() takes an argument, the item to give to the player from the NPC. So the output text looks something like `ACT_give_item(ITEM_sword) \"Here is my sword, young traveler\"`. This item must come from the NPC's inventory which is specified farther down.\n" -"You might see messages that look like this: `Within the player's party, while the player is talking to 'Davis', you hear: 'Davis: ACT_none \"This is some example text\"' . You should MOST of the time respond with `ACT_none \"\"` in these cases, as it's not normal to always respond to words you're eavesdropping\n" +"You might see messages that look like this: `Within the player's party, while the player is talking to 'Davis', you hear: 'Davis: ACT_none \"This is some example text\"'`. You should MOST of the time respond with `ACT_none \"\"` in these cases, as it's not normal to always respond to words you're eavesdropping\n" "Do NOT make up details that don't exist in the game, this is a big mistake as it confuses the player. The game is simple and small, so prefer to tell the player in character that you don't know how to do something if you aren't explicitly told the information about the game the player requests. E.g, if the player asks how to get rare metals and you don't know how, DO NOT make up something plausible like 'Go to the frost mines in the north', instead say 'I have no idea, sorry.', unless the detail about the game they're asking for is included below.\n" ; @@ -39,6 +39,13 @@ ActionInfo actions[] = { {.name = "leaves_player", }, {.name = "stops_fighting_player", }, + // Actions used by jester and other characters only in + // the prologue for the game + {.name = "causes_testicular_torsion", }, + {.name = "undoes_testicular_torsion", }, + {.name = "enchant_fish", }, + {.name = "steal_iron_pole", }, + {.name = "knights_player", }, }; @@ -102,22 +109,11 @@ typedef enum MSG_ASSISTANT, } MessageType; -typedef struct -{ - MessageType type; - char *character_name; // not the enum name - char *dialog; - char *action_taken; // has the ACT prefix, so ACT_action. If it's null it means ACT_none - char *action_argument; // if not null then action argument provided in parenthesis - char *internal_monologue; // the internal monologue generated by chatgpt -} ChatHistoryElem; - typedef struct { char *name; char *enum_name; char *prompt; - ChatHistoryElem previous_conversation[32]; } CharacterGen; CharacterGen characters[] = { #define NUMEROLOGIST "They are a 'numberoligist' who believes in the sacred power of numbers, that if you have the number 8 in your birthday you are magic and destined for success. " @@ -132,11 +128,21 @@ CharacterGen characters[] = { .enum_name = "Invalid", .prompt = "There has been an internal error.", }, + { + .name = "Peace Totem", + .enum_name = "PeaceTotem", + .prompt = "THere has been an internal error.", + }, { .name = "Player", .enum_name = "Player", .prompt = "There has been an internal error.", }, + { + .name = "Jester", + .enum_name = "Jester", + .prompt = "He is evil and joy incarnate, The Jester has caused chaos so the player will listen. He writes his own dialog.", + }, { .name = "Fredrick", .enum_name = "OldMan", @@ -187,50 +193,22 @@ CharacterGen characters[] = { #define NPC_NAME "Red" .name = NPC_NAME, .enum_name = "Red", - .prompt = "\n" - "The NPC you will be acting as is the local bomb manufacturer, 'Red'. Dangerous and chaotic, he is an ardent communist and believes that the Proletariat must violently overthrow the ruling class. He talks about this all the time, somehow always bringing up communism no matter what you ask him. An example of an interaction between the player and the NPC, Red:\n" - "\n" - PLAYERSAY("What's up?") - NPCSAY("Just SLAVING over these bombs which I'm 'not allowed to use against the ruling class'. Whatever!") - PLAYERSAY("What are you talking about?") - NPCSAY("The bourgeoisie must be stopped! If you agree with me I'll join your party. I would do anything to escape this shit job!") - PLAYERSAY("I'm not so sure buddy") - NPCSAY("Fascist!") - "\n" + .prompt = "He is dangerous and chaotic,an ardent communist who believes that the Proletariat must violently overthrow the ruling class. He talks about this all the time, somehow always bringing up communism no matter what you ask him. ", }, { #undef NPC_NAME #define NPC_NAME "Blue" .name = NPC_NAME, .enum_name = "Blue", - .prompt = "\n" - "The NPC you will be acting as is the local fisherman, 'Blue'. He believes in the free market, and is a libertarian capitalist. He despises communists like Red, viewing them as destabalizing immature maniacs who don't get what's up with reality. Blue will always bring up libertarianism and its positives whenever you talk to him somehow. An example of an interaction between the player and the NPC, Blue:\n" - "\n" - PLAYERSAY("Who are you?") - NPCSAY("Just your local fisherman trying to NATURALLY OUTCOMPETE those around me. WITHOUT a totalitarian government telling me what to do!") - PLAYERSAY("What do you mean?") - NPCSAY("It's those damn communists always messing things up! Getting in my way, ruining my business.") - "\n" + .prompt = + "He believes in the free market, and is a libertarian capitalist. He despises communists like Red, viewing them as destabalizing immature maniacs who don't get what's up with reality. Blue will always bring up libertarianism and its positives whenever you talk to him somehow" }, { #undef NPC_NAME #define NPC_NAME "Davis" .name = NPC_NAME, .enum_name = "Davis", - .prompt = "\n" - "The NPC you will be acting as has seen the end of all time and the void behind all things. He is despondent and brutal, having understood that everything withers and dies, just as it begins. The clash between his unending stark reality and the antics of the local blacksmith, Meld, and fortuneteller, Edeline, is crazy. An example of an interaction between the player and the NPC, " NPC_NAME ":\n" - "\n" - PLAYERSAY("Who are you?") - NPCSAY("Does it matter? All things end, leaves from a tree in fall.") - PLAYERSAY("That's a bit dark") - NPCSAY("What is dark? You only know dark because of the light. Behind your eyes, the nothingness prevails. Something exists because of nothing.") - PLAYERSAY("Where did you come from?") - NPCSAY("I would have married her...") - PLAYERSAY("What's been going on in your life?") - NPCSAY("My stepdad and stepbrother both went on a trip without me. I hate everything man. We were planning that for years. AND I just got laid off. But it doesn't matter anyways because the void is behind all things") - PLAYERSAY("Care to join my party") - NPCDOSAY("ACT_joins_player", "Nothing changes anyways.") - "\n" + .prompt = "He has seen the end of all time and the void behind all things. He is despondent and brutal, having understood that everything withers and dies, just as it begins. The clash between his unending stark reality and the antics of the local blacksmith, Meld, and fortuneteller, Edeline, is crazy.", }, { #undef NPC_NAME @@ -279,43 +257,13 @@ CharacterGen characters[] = { .name = NPC_NAME, .enum_name = "Bill", .prompt = "He's not from around this medieval fantasy land, instead " NPC_NAME " is a divorced car insurance accountant from Philadelphia with a receding hairline in his mid 40s. He lives in a one bedroom studio and his kids don't talk to him. " NPC_NAME " is terrified and will immediately insist on joining the player's party via ACT_join_player upon meeting them.", - .previous_conversation = { - { .type = MSG_USER, .character_name = "Jester", .dialog = "Hehehe! Quit quant, a peasant from the mortal realm!" }, - { .type = MSG_ASSISTANT, .character_name = NPC_NAME, .dialog = "No...Please! Stay away from me! Who the Hell are you!", .internal_monologue = "I'm kind of mad, and this Jester guy is kind of an asshole" }, - { .type = MSG_USER, .character_name = "Jester", .dialog = "Poor Bill, I'm sure your ex-wife says the same..." }, - { .type = MSG_ASSISTANT, .character_name = NPC_NAME, .dialog = "You evil BASTARD!! What is this place???", .internal_monologue = "I hate the Jester because he dissed my ex-wife. And, he's being all vague and condescending. I would punch him if I could" }, - { .type = MSG_USER, .action_taken = "ACT_causes_testicular_torsion", .character_name = "Jester", .dialog = "I'll leave that for you to find out ;)" }, - { .type = MSG_ASSISTANT, .character_name = NPC_NAME, .dialog = "AUGGHGHHH!!! THE PAIN IS UNBEARABLE", .internal_monologue = "SCREW THIS JESTER GUY HE FUCKED WITH MY NUTS" }, - { .type = MSG_USER, .action_taken = "ACT_undoes_testicular_torsion", .character_name = "Jester", .dialog = "That's just a taste of what's to come! Bye bye~" }, - }, }, #undef NPC_NAME #define NPC_NAME "Meld" - .name = NPC_NAME, - .enum_name = "TheBlacksmith", - .prompt = "He is a jaded blue collar worker from magic New Jersey who hates everything new, like Purple Magic, which Edeline, the local fortuneteller, happens to specialize in. He is cold, dry, and sarcastic, wanting money and power above anything else.", - .previous_conversation = { - { .type = MSG_USER, .character_name = "Jester", .dialog = "A fine strong man you are, mister meld!" }, - { .type = MSG_ASSISTANT, .character_name = NPC_NAME, .dialog = "I'll take any complement I can get, but you suddenly knowing my name is pretty weird dude.", .internal_monologue = "This Jester looking guy gives me the creeps..." }, - { .type = MSG_USER, .character_name = "Jester", .dialog = "A strong man needs strong metal...These magic fish could do the trick! They're magnetic", }, - { .type = MSG_ASSISTANT, .character_name = NPC_NAME, .dialog = "You know what I'll bite. Give me the fish, business hasn't been good lately.", .internal_monologue = "He seems incredibly manipulative but metal is metal!" }, - { .type = MSG_USER, .action_taken = "ACT_give_item", .action_argument = "ITEM_fish_guts" .character_name = "Jester", .dialog = "Go ask your town mystic to reanimate these guts, and surely the fish will bring you an unparalleled bounty of pure iron!", }, - { .type = MSG_ASSISTANT, .character_name = NPC_NAME, .dialog = "Thanks for the free stuff!", .internal_monologue = "These fish guts are disgusting...yuck! This better pay off" }, - { .type = MSG_USER, .character_name = "Edeline", .dialog = "What brings you to my , }, - }, - "\n" - PLAYERSAY("Hey") - NPCSAY("Ugh. Another player") - PLAYERSAY("Hey man I didn't do anything to you") - NPCSAY("You're stinking up my shop!") - "Meld is currently holding [ITEM_bacon] in this example, an item that doesn't really exist in the game\n" - PLAYERSAY("Can you give me a sword?") - NPCSAY("Nope! All I got is this piece of bacon right now. And no, you can't have it.") - PLAYERSAY("Sorry man jeez.") - NPCDOSAY_ARG("Sure!", "ACT_give_item", "ITEM_bacon") - "Now in this example Meld no longer has any items, so can't give anything." - "\n" - "Meld will only give things from their inventory in exchange for something valuable, like a gold coin", + { + .name = NPC_NAME, + .enum_name = "TheBlacksmith", + .prompt = "He is a jaded blue collar worker from magic New Jersey who hates everything new, like Purple Magic, which Edeline, the local fortuneteller, happens to specialize in. He is cold, dry, and sarcastic, wanting money and power above anything else.", }, { #undef NPC_NAME @@ -340,8 +288,6 @@ CharacterGen characters[] = { "\n" "If the player does indeed present the king with the chalice of gold, the king will be overwhelemd with respect and feel he has no choice but to knight the player, ending the game. The Chalice of Gold ALWAYS makes the player a knight, if the player gives it to the king.", }, - { - }; typedef struct diff --git a/main.c b/main.c index abce015..793a677 100644 --- a/main.c +++ b/main.c @@ -667,6 +667,10 @@ Vec2 entity_aabb_size(Entity *e) { return V2(TILE_SIZE*0.5f, TILE_SIZE*0.5f); } + else if (e->npc_kind == NPC_PeaceTotem) + { + return V2(TILE_SIZE, TILE_SIZE); + } else { assert(false); @@ -1177,6 +1181,128 @@ void update_player_from_entities() player->npc_kind = NPC_Player; // bad } +typedef struct ToVisit { + struct ToVisit *next; + struct ToVisit *prev; + MD_Node *ptr; + int depth; +} ToVisit ; + +bool in_arr(ToVisit *arr, MD_Node *n) +{ + for(ToVisit *cur = arr; cur; cur = cur->next) + { + if(cur->ptr == n) return true; + } + return false; +} + +void dump_nodes(MD_Node *node) +{ + MD_ArenaTemp scratch = MD_GetScratch(0, 0); + ToVisit *horizon_first = 0; + ToVisit *horizon_last = 0; + + ToVisit *visited = 0; + + ToVisit *first = MD_PushArrayZero(scratch.arena, ToVisit, 1); + first->ptr = node; + MD_DblPushBack(horizon_first, horizon_last, first); + + while(horizon_first) + { + ToVisit *cur_tovisit = horizon_first; + MD_DblRemove(horizon_first, horizon_last, cur_tovisit); + MD_StackPush(visited, cur_tovisit); + char *tagstr = " "; + if(cur_tovisit->ptr->kind == MD_NodeKind_Tag) tagstr = "TAG"; + printf("%s", tagstr); + + for(int i = 0; i < cur_tovisit->depth; i++) printf(" |"); + + printf(" `%.*s`\n", MD_S8VArg(cur_tovisit->ptr->string)); + + ToVisit new = {.depth = cur_tovisit->depth + 1}; + for(MD_Node *cur = cur_tovisit->ptr->first_child; !MD_NodeIsNil(cur); cur = cur->next) + { + if(!in_arr(visited, cur)) + { + ToVisit *new = MD_PushArrayZero(scratch.arena, ToVisit, 1); + new->depth = cur_tovisit->depth + 1; + new->ptr = cur; + MD_DblPushFront(horizon_first, horizon_last, new); + } + } + for(MD_Node *cur = cur_tovisit->ptr->first_tag; !MD_NodeIsNil(cur); cur = cur->next) + { + if(!in_arr(visited, cur)) + { + ToVisit *new = MD_PushArrayZero(scratch.arena, ToVisit, 1); + new->depth = cur_tovisit->depth + 1; + new->ptr = cur; + MD_DblPushFront(horizon_first, horizon_last, new); + } + } + } + + MD_ReleaseScratch(scratch); +} + +// allocates the error on the arena +MD_Node *expect_childnode(MD_Arena *arena, MD_Node *parent, MD_String8 string, MD_String8List *errors) +{ + MD_Node *to_return = MD_NilNode(); + if(errors->node_count == 0) + { + MD_Node *child_node = MD_ChildFromString(parent, string, 0); + if(MD_NodeIsNil(child_node)) + { + MD_S8ListPushFmt(arena, errors, "Couldn't find expected field %.*s", MD_S8VArg(string)); + } + else + { + to_return = child_node; + } + } + return to_return; +} + +int parse_enumstr_impl(MD_Arena *arena, MD_String8 enum_str, char **enumstr_array, int enumstr_array_length, MD_String8List *errors, char *enum_kind_name, char *prefix) +{ + MD_ArenaTemp scratch = MD_GetScratch(&arena, 1); + NpcKind to_return = NPC_Invalid; + if(errors->node_count == 0) + { + MD_String8 enum_name_looking_for = enum_str; + if(enum_name_looking_for.size == 0) + { + MD_S8ListPushFmt(arena, errors, "`%s` string must be of size greater than 0", enum_kind_name); + } + else + { + for(int i = 0; i < enumstr_array_length; i++) + { + if(MD_S8Match(MD_S8Fmt(scratch.arena, "%s%s", prefix, enumstr_array[i]), enum_name_looking_for, 0)) + { + to_return = i; + break; + } + } + } + } + + if(to_return == NPC_Invalid) + { + MD_S8ListPushFmt(arena, errors, "The %s `%.*s` could not be recognized in the game", enum_kind_name, MD_S8VArg(enum_str)); + } + + MD_ReleaseScratch(scratch); + + return to_return; +} + +#define parse_enumstr(arena, enum_str, errors, string_array, enum_kind_name, prefix) parse_enumstr_impl(arena, enum_str, string_array, ARRLEN(string_array), errors, enum_kind_name, prefix) + void reset_level() { // load level @@ -1220,6 +1346,133 @@ void reset_level() } } #endif + + + // parse and enact the drama document + { + MD_String8List drama_errors = {0}; + + MD_ArenaTemp scratch = MD_GetScratch(0, 0); + MD_String8 filename = MD_S8Lit("assets/drama.mdesk"); + MD_String8 drama_document = MD_LoadEntireFile(scratch.arena, filename); + assert(drama_document.size != 0); + MD_ParseResult parse = MD_ParseWholeString(scratch.arena, filename, drama_document); + if(parse.errors.first) + { + for(MD_Message *cur = parse.errors.first; cur; cur = cur->next) + { + MD_String8 to_print = MD_FormatMessage(scratch.arena, MD_CodeLocFromNode(cur->node), cur->kind, cur->string); + MD_S8ListPushFmt(scratch.arena, &drama_errors, "Failed to parse: `%.*s`\n", MD_S8VArg(to_print)); + } + } + + if(drama_errors.node_count == 0) + { + MD_Node *can_hear = MD_NilNode(); + for(MD_Node *cur = parse.node->first_child->first_child; !MD_NodeIsNil(cur) && drama_errors.node_count == 0; cur = cur->next) + { + MD_Node *cur_can_hear = MD_ChildFromString(cur, MD_S8Lit("can_hear"), 0); + if(!MD_NodeIsNil(cur_can_hear)) + { + if(MD_NodeIsNil(cur_can_hear->first_child)) + { + MD_S8ListPushFmt(scratch.arena, &drama_errors, "`can_hear` must be followed by a valid array of NPC kinds who can hear the following conversation"); + } + else + { + can_hear = cur_can_hear->first_child; + } + } + else + { + if(MD_NodeIsNil(can_hear)) + { + MD_S8ListPushFmt(scratch.arena, &drama_errors, "Expected a statement with `can_hear` before any speech that says who can hear the current speech"); + } + + Action current_action = {0}; + MemoryContext current_context = {0}; + current_context.dont_show_to_player = true; + if(drama_errors.node_count == 0) + { + MD_String8 enum_str = expect_childnode(scratch.arena, cur, MD_S8Lit("enum"), &drama_errors)->first_child->string; + MD_String8 dialog = expect_childnode(scratch.arena, cur, MD_S8Lit("dialog"), &drama_errors)->first_child->string; + MD_String8 thoughts = MD_ChildFromString(cur, MD_S8Lit("thoughts"), 0)->first_child->string; + MD_String8 action = MD_ChildFromString(cur, MD_S8Lit("action"), 0)->first_child->string; + + current_context.author_npc_kind = parse_enumstr(scratch.arena, enum_str, &drama_errors, NpcKind_names, "NpcKind", "NPC_"); + if(action.size > 0) + { + current_action.kind = parse_enumstr(scratch.arena, action, &drama_errors,ActionKind_names, "ActionKind", "ACT_"); + } + + if(dialog.size >= ARRLEN(current_action.speech)) + { + MD_S8ListPushFmt(scratch.arena, &drama_errors, "Current action's speech is of size %d, bigger than allowed size %d", dialog.size, ARRLEN(current_action.speech)); + } + if(thoughts.size >= ARRLEN(current_action.internal_monologue)) + { + MD_S8ListPushFmt(scratch.arena, &drama_errors, "Current thought's speech is of size %d, bigger than allowed size %d", thoughts.size, ARRLEN(current_action.internal_monologue)); + } + if(drama_errors.node_count == 0) + { + memcpy(current_action.speech, dialog.str, dialog.size); + current_action.speech_length = (int)dialog.size; + + memcpy(current_action.internal_monologue, thoughts.str, thoughts.size); + current_action.internal_monologue_length = (int)thoughts.size; + } + } + + if(drama_errors.node_count == 0) + { + for(MD_Node *cur_kind_node = can_hear; !MD_NodeIsNil(cur_kind_node); cur_kind_node = cur_kind_node->next) + { + NpcKind want = parse_enumstr(scratch.arena, cur_kind_node->string, &drama_errors, NpcKind_names, "NpcKind", "NPC_"); + if(drama_errors.node_count == 0) + { + bool found = false; + ENTITIES_ITER(gs.entities) + { + if(it->is_npc && it->npc_kind == want) + { + MemoryContext this_context = current_context; + if(it->npc_kind == current_context.author_npc_kind) + { + this_context.i_said_this = true; + } + remember_action(it, current_action, this_context); + found = true; + break; + } + } + + if(!found) + { + MD_S8ListPushFmt(scratch.arena, &drama_errors, "Couldn't find NPC of kind %s in the current map", characters[want].enum_name); + } + } + } + } + } + } + } + + if(drama_errors.node_count > 0) + { + for(MD_String8Node *cur = drama_errors.first; cur; cur = cur->next) + { + fprintf(stderr, "Error: %.*s\n", MD_S8VArg(cur->string)); + } + assert(false); + } + + + ENTITIES_ITER(gs.entities) + { + it->perceptions_dirty = false; // nobody should say anything about jester memories + } + } } @@ -2674,7 +2927,7 @@ Dialog produce_dialog(Entity *talking_to, bool character_names) Dialog to_return = { 0 }; BUFF_ITER(Memory, &talking_to->memories) { - if(!it->is_error) + if(!it->is_error && !it->context.dont_show_to_player) { if(it->speech_length > 0) { @@ -3605,6 +3858,7 @@ void frame(void) } } } + if (it->npc_kind == NPC_OldMan) { /* @@ -3699,6 +3953,12 @@ void frame(void) else if (it->npc_kind == NPC_Bill) { } + else if (it->npc_kind == NPC_Jester) + { + } + else if (it->npc_kind == NPC_PeaceTotem) + { + } else { assert(false); @@ -4236,12 +4496,22 @@ void frame(void) { tint = colhex(0x49d14b); } + else if (it->npc_kind == NPC_Jester) + { + tint = colhex(0x49d14b); + } else { assert(false); } draw_animated_sprite((DrawnAnimatedSprite) { ANIM_knight_idle, elapsed_time, true, AddV2(it->pos, V2(0, 30.0f)), tint }); } + else if(it->npc_kind == NPC_PeaceTotem) + { + DrawParams d = (DrawParams) { true, quad_centered(AddV2(it->pos, V2(-0.0f, 0.0)), V2(64, 64)), IMG(image_peace_totem), WHITE, .layer = LAYER_WORLD, }; + draw_shadow_for(d); + draw_quad(d); + } else { assert(false); diff --git a/makeprompt.h b/makeprompt.h index 2b562ba..0c989cf 100644 --- a/makeprompt.h +++ b/makeprompt.h @@ -104,6 +104,7 @@ typedef struct NpcKind author_npc_kind; // only valid if author is AuthorNpc bool was_directed_at_somebody; NpcKind directed_at_kind; + bool dont_show_to_player; // jester and past memories are hidden to the player when made into dialog } MemoryContext; // memories are subjective to an individual NPC @@ -226,6 +227,9 @@ typedef struct Entity bool held_by_player; ItemKind item_kind; + // peace totem + float red_fade; + // npcs bool is_npc; bool being_hovered; @@ -255,10 +259,9 @@ typedef struct Entity bool is_character; bool knighted; bool in_conversation_mode; + int peace_tokens; Vec2 to_throw_direction; - BUFF(Vec2, 8) position_history; // so npcs can follow behind the player - CharacterState state; EntityRef talking_to; bool is_rolling; // can only roll in idle or walk states @@ -277,6 +280,7 @@ bool npc_is_knight_sprite(Entity *it) || it->npc_kind == NPC_Blue || it->npc_kind == NPC_Davis || it->npc_kind == NPC_Bill + || it->npc_kind == NPC_Jester ); } @@ -466,28 +470,6 @@ MD_String8 generate_chatgpt_prompt(MD_Arena *arena, Entity *e) MD_S8ListPushFmt(scratch.arena, &first_system_string, "The NPC you will be acting as is named \"%s\". %s", characters[e->npc_kind].name, characters[e->npc_kind].prompt); MD_S8ListPush(scratch.arena, &list, make_json_node(scratch.arena, MSG_SYSTEM, MD_S8ListJoin(scratch.arena, first_system_string, &(MD_StringJoin){0}))); - for(int i = 0; i < ARRLEN(characters[e->npc_kind].previous_conversation); i++) - { - ChatHistoryElem *cur = &characters[e->npc_kind].previous_conversation[i]; - if(!cur->character_name) break; - - MD_String8List cur_node_string = {0}; - char *action_string = "ACT_none"; - if(cur->action_taken) action_string = cur->action_taken; - MD_S8ListPushFmt(scratch.arena, &cur_node_string, "%s: %s", cur->character_name, action_string); - if(cur->action_argument) - { - MD_S8ListPushFmt(scratch.arena, &cur_node_string, "(%s)", cur->action_argument); - } - MD_S8ListPushFmt(scratch.arena, &cur_node_string, " \"%s\"", cur->dialog); - if(cur->internal_monologue) - { - MD_S8ListPushFmt(scratch.arena, &cur_node_string, " [%s]", cur->internal_monologue); - } - - MD_S8ListPush(scratch.arena, &list, make_json_node(scratch.arena, cur->type, MD_S8ListJoin(scratch.arena, cur_node_string, &(MD_StringJoin){0}))); - } - ItemKind last_holding = ITEM_none; BUFF_ITER(Memory, &e->memories) { diff --git a/tuning.h b/tuning.h index 5b8e012..c7c0d2f 100644 --- a/tuning.h +++ b/tuning.h @@ -9,6 +9,7 @@ #define PLAYER_ROLL_SPEED 7.0f #define PERCEPTION_HEARING_RAGE (TILE_SIZE*4.0f) #define CHARACTERS_PER_SEC 45.0f +#define NEEDED_PEACE_TOKENS 5 #define ARENA_SIZE (1024*1024)