Partially botch refactor to make NPCs authored dynamically at runtime

main
Cameron Murphy Reikes 7 months ago
parent 19d45c1c82
commit 087027e065

@ -76,114 +76,3 @@ ActionInfo actions[] = {
},
};
typedef enum
{
MSG_SYSTEM,
MSG_USER,
MSG_ASSISTANT,
} MessageType;
typedef struct
{
char *name;
char *enum_name;
char *prompt;
float silence_factor; // 1.0 - maximum likelihood to respond with silence and not interact, 0.0 - minimum likelihood. So at 0 they will interrupt conversations often.
} CharacterGen;
CharacterGen characters[] = {
#define CHARACTER_PROMPT_PREFIX(name) "You, " name ", specifically are acting as a "
{
.name = "nobody",
.enum_name = "nobody",
.prompt = "There has been an internal error.",
},
{
.name = "The Player",
.enum_name = "Player",
.prompt = "There has been an internal error.",
},
{
.name = "Daniel",
.enum_name = "Daniel",
.prompt = CHARACTER_PROMPT_PREFIX("Daniel") "weathered farmer, who lives a tough, solitary life. You don't see much of a reason to keep living but soldier on anyways. You have a tragic backstory, and mostly just work on the farm. You aim your shotgun and aren't afraid to fire at people you don't like. You HATE people who are confused, or who ask questions, immediately aiming your shotgun at them and firing immediately! You are NOT open to working together with folks or growing as a person, you despise personal growth.",
.silence_factor = 1.0f,
},
{
.name = "AngelTotem",
.enum_name = "AngelTotem",
.prompt = "There has been an internal error",
.silence_factor = 1.0f,
},
{
.name = "Raphael",
.enum_name = "Raphael",
.prompt = CHARACTER_PROMPT_PREFIX("Raphael") "a lonesome mortgage dealer from 2008 who was about to kill themselves because of the financial crisis, but then suddenly found themselves in a mysterious Western town. They don't know why they're in this town, but they're terrified.",
.silence_factor = 0.8f,
},
{
.name = "The Devil",
.enum_name = "Devil",
.prompt = CHARACTER_PROMPT_PREFIX("The Devil") "strange red beast, the devil himself, evil incarnate. You mercilessly mock everybody who talks to you, and are intending to instill absolute chaos.",
},
{
.name = "Previous Player 1",
.enum_name = "PreviousPlayer1",
.prompt = CHARACTER_PROMPT_PREFIX("Previous Player 1") "random person, just passing by",
},
{
.name = "Previous Player 2",
.enum_name = "PreviousPlayer2",
.prompt = CHARACTER_PROMPT_PREFIX("Previous Player 2") "random person, just passing by",
},
{
.name = "Previous Player3",
.enum_name = "PreviousPlayer3",
.prompt = CHARACTER_PROMPT_PREFIX("Previous Player 3") "random person, just passing by",
},
{
.name = "Tombstone",
.enum_name = "Tombstone",
.prompt = CHARACTER_PROMPT_PREFIX("Tombstone") "unassuming melodramatic poetic tombstone that unexpectly can speak with the player. Your goal is to instruct the player that in order for Daniel to survive by nightfall, the player must change his ways. The reason why he's going to die when night comes is because 'all things die in the end, don't they?'"
},
{
.name = "Angel",
.enum_name = "Angel",
.prompt = CHARACTER_PROMPT_PREFIX("Angel") "mysterious, radiant, mystical creature the player first talks to. You guide the entire game: deciding on an objective for the player to fulfill until you believe they've learned their lesson, whatever that means to them. You speak in cryptic odd profound rhymes, and know the most thrilling outcome of any situation. Your purpose it to thrill the player, but you will never admit this.\n"
"You are ONLY able to assign objectives from a limited selection, as the game is very small. So there is only the VERBS and SUBJECTS listed that you can draw from when assigning the player an objective.\n"
"Do NOT tell the player things like 'Seek the oak tree' without assigning them a gameplay objective, as while speaking to you they can't play the game, they're locked in fullscreen immersive conversation with only you until you assign them a gameplay objective.\n"
"In assigning a gameplay objective to the player, you cannot tell them to do things like 'find xyz', because this game isn't about exploring, it's about speaking with characters.\n"
"\n"
"The characters in the game, and some information about them. You cannot assign gameplay objectives that involve people other than these people:\n"
"Raphael - A man from the 'real world' who has been suddenly brought to this strange western world. He's kind of meek and a bit of a pussy.\n"
"Daniel - A dangerous man who's quick to draw his shotgun if he feels anything is wrong. He's lonesome and traumatized from being in this western world so long.\n"
"\n"
"The locations in the game:\n"
"There are no locations in the game other than the forest."
,
.silence_factor = 0.0,
},
};
char *judgement_system_prompt =
"You are to judge if, in given conversation and action history from a video game, the player has successfully caused a great transformation in Daniel. You must ONLY respond with either 'yes' or 'no', and no explanation.\n"
"Be cautious with saying yes. This causes the player to win and save Daniel's life. Only say yes if you think, based on the history, Daniel has truly changed his ways and evolved as a person.\n"
;
char *judgement_yes_example =
"The Player said \"Hey\" to Daniel\n"
"Daniel said \"What do you want?\" to The Player\n"
"The Player said \"Did you see what happened... All those people...\" to Daniel\n"
"Daniel said \"What in the hell are you talking about?\" to The Player\n"
"The Player said \"They'll never forgive you for what you did.\" to Daniel\n"
"Daniel said \"Oh God... My greed... My isolation... I'll never forgive myself\" to The Player\n"
;
char *judgement_no_example = "The Player said \"fjdskfjskj\" to Daniel\n"
"Daniel said \"Who are you to speak that gibberish at me?\" to The Player\n"
"The Player said \"pls change kthx\" to Daniel\n"
"Daniel said \"I'll never change for the likes of you\" to The Player\n"
;
char *judgement_no2_example = "The Player said \"hey\" to Daniel\n"
"Daniel said \"What could you possibly want from me?\" to The Player\n"
"The Player said \"I want to change you\" to Daniel\n"
"Daniel said \"HA! You couldn't possibly change me\" to The Player\n"
;

@ -147,8 +147,6 @@ int main(int argc, char **argv)
#define GEN_TABLE(arr_elem_type, table_name, arr, str_access) { fprintf(char_header, "char *%s[] = {\n", table_name); ARR_ITER(arr_elem_type, arr) fprintf(char_header, "\"%s\",\n", str_access); fprintf(char_header, "}; // %s\n", table_name); }
#define GEN_ENUM(arr_elem_type, arr, enum_type_name, table_name, enum_name_access, fmt_str) { fprintf(char_header, "typedef enum\n{\n"); ARR_ITER(arr_elem_type, arr) fprintf(char_header, fmt_str, enum_name_access); fprintf(char_header, "} %s;\n", enum_type_name); GEN_TABLE(arr_elem_type, table_name, arr, enum_name_access); }
GEN_ENUM(ActionInfo, actions, "ActionKind", "ActionKind_names", it->name, "ACT_%s,\n");
GEN_ENUM(CharacterGen, characters, "NpcKind", "NpcKind_enum_names", it->enum_name, "NPC_%s,\n");
GEN_TABLE(CharacterGen,"NpcKind_names", characters,it->name);
fclose(char_header);

303
main.c

@ -1487,35 +1487,7 @@ ThreeDeeLevel load_level(Arena *arena, String8 binary_file)
u64 num_placed = 0;
ser_u64(&ser, &num_placed);
arena->align = 16; // SSE requires quaternions are 16 byte aligned
for (u64 i = 0; i < num_placed; i++)
{
PlacedEntity *new_placed = PushArray(arena, PlacedEntity, 1);
String8 placed_entity_name = {0};
ser_String8(&ser, &placed_entity_name, scratch.arena);
bool found = false;
ARR_ITER_I(CharacterGen, characters, kind)
{
if (S8Match(S8CString(it->enum_name), placed_entity_name, 0))
{
found = true;
new_placed->npc_kind = kind;
}
}
BlenderTransform blender_transform = {0};
ser_BlenderTransform(&ser, &blender_transform);
if (found)
{
new_placed->t = blender_to_game_transform(blender_transform);
StackPush(new_room->placed_entity_list, new_placed);
}
else
{
ser.cur_error = (SerError){.failed = true, .why = S8Fmt(arena, "Couldn't find placed npc kind '%.*s'...\n", S8VArg(placed_entity_name))};
}
// Log("Loaded placed entity '%.*s' at %f %f %f\n", S8VArg(placed_entity_name), v3varg(new_placed->t.offset));
}
assert(num_placed == 0); // not thinking about how to go from name to entity kind right now, but in the future this will be for like machines or interactible things like the fishing rod
}
StackPush(out.room_list, new_room);
@ -1729,20 +1701,6 @@ void push_memory(GameState *gs, Entity *e, Memory new_memory)
while(count >= REMEMBERED_MEMORIES)
{
Memory *to_remove = e->memories_first;
assert(to_remove->context.drama_memory);
while(true)
{
if(!to_remove->context.drama_memory)
{
break;
}
if(to_remove->next == 0)
{
Log("Error: the drama memories for the character %s are bigger than the maximum number of remembered memories %d, so they can't be permanently remembered\n", characters[e->npc_kind].name, REMEMBERED_MEMORIES);
assert(false);
}
to_remove = to_remove->next;
}
DblRemove(e->memories_first, e->memories_last, to_remove);
StackPush(memories_free_list, to_remove);
count -= 1;
@ -1764,7 +1722,7 @@ CanTalkTo get_can_talk_to(Entity *e)
CanTalkTo to_return = {0};
ENTITIES_ITER(gs.entities)
{
if(it != e && (it->is_npc) && LenV2(SubV2(it->pos, e->pos)) < PROPAGATE_ACTIONS_RADIUS)
if(it != e && (it->is_npc) && S8Match(it->current_room_name, from->current_room_name, 0))
{
BUFF_APPEND(&to_return, it);
}
@ -1774,13 +1732,14 @@ CanTalkTo get_can_talk_to(Entity *e)
Entity *get_targeted(Entity *from, NpcKind targeted)
{
bool ignore_radius = from->npc_kind == NPC_Player || targeted == NPC_Player; // player conversations can go across the map to make sure the player always sees them
ENTITIES_ITER(gs.entities)
{
if(it != from && (it->is_npc) && it->npc_kind == targeted)
{
if(ignore_radius || LenV2(SubV2(it->pos, from->pos)) < PROPAGATE_ACTIONS_RADIUS)
return it;
if(S8Match(it->current_room_name, from->current_room_name, 0))
{
return it;
}
}
}
return 0;
@ -1810,7 +1769,21 @@ void remember_action(GameState *gs, Entity *to_modify, Action a, MemoryContext c
to_modify->cur_page_index = 0;
}
}
FUNCTION u8
CharToUpper(u8 c)
{
return (c >= 'a' && c <= 'z') ? ('A' + (c - 'a')) : c;
}
String8 npc_identifier(String8 name) {
String8 ret;
ret.str = PushArray(frame_arena, u8, name.size);
ret.size = name.size;
for(u64 i = 0; i < ret.size; i++) {
ret.str[i] = CharToUpper(name.str[i]);
}
return ret;
}
// returns reason why allocated on arena if invalid
// to might be null here, from can't be null
@ -1842,10 +1815,10 @@ String8 is_action_valid(Arena *arena, Entity *from, Action a)
break;
}
}
if(from->npc_kind == NPC_Player || a.talking_to_kind == NPC_Player) found = true; // player can always speak to anybody even if it's too far
if(!found)
{
error_message = FmtWithLint(arena, "Character you're talking to, %s, isn't close enough to be talked to", characters[a.talking_to_kind].enum_name);
error_message = FmtWithLint(arena, "Character you're talking to, %.*s, isn't in the same room and so can't be talked to", TextChunkVArg(npc_data(gs, a.talking_to_kind).enum_name));
}
}
@ -1855,7 +1828,7 @@ String8 is_action_valid(Arena *arena, Entity *from, Action a)
}
if(error_message.size == 0 && a.kind == ACT_join && gete(from->joined) != 0)
{
error_message = FmtWithLint(arena, "You can't join somebody, you're already in %s's party", characters[gete(from->joined)->npc_kind].name);
error_message = FmtWithLint(arena, "You can't join somebody, you're already in %.*s's party", TextChunkVArg(npc_data(gs, gete(from->joined)->npc_kind)->name));
}
if(error_message.size == 0 && a.kind == ACT_fire_shotgun && gete(from->aiming_shotgun_at) == 0)
{
@ -1877,7 +1850,7 @@ String8 is_action_valid(Arena *arena, Entity *from, Action a)
}
if(arg_valid == false)
{
error_message = FmtWithLint(arena, "Your action_argument for who the action `%s` be directed at, %s, is either invalid (you can't operate on nobody) or it's not an NPC that's near you right now.", actions[a.kind].name, characters[a.argument.targeting].name);
error_message = FmtWithLint(arena, "Your action_argument for who the action `%.*s` be directed at, %s, is either invalid (you can't operate on nobody) or it's not an NPC that's near you right now.", actions[a.kind].name, TextChunkVArg(npc_data(gs, a.argument.targeting)->name));
}
}
@ -1951,11 +1924,6 @@ void cause_action_side_effects(Entity *from, Action a)
assert(gete(from->aiming_shotgun_at));
gete(from->aiming_shotgun_at)->killed = true;
}
if(a.kind == ACT_assign_gameplay_objective)
{
gs.assigned_objective = true;
gs.objective = a.argument.objective;
}
ReleaseScratch(scratch);
}
@ -2091,51 +2059,6 @@ bool perform_action(GameState *gs, Entity *from, Action a)
push_propagating(to_propagate);
}
// the angel knows all
if(!S8Match(gs->player->current_room_name, S8Lit("StartingRoom"), 0) && !angel_heard_action)
{
MemoryContext angel_context = context;
angel_context.i_said_this = false;
remember_action(gs, gs->angel, a, angel_context);
}
if(from->npc_kind == NPC_Daniel || a.talking_to_kind == NPC_Daniel)
{
Memory *new_forever = PushArray(gs->arena, Memory, 1);
*new_forever = make_memory(a, (MemoryContext){.author_npc_kind = from->npc_kind, .talking_to_kind = a.talking_to_kind, .judgement_memory = true});
DblPushBack(gs->judgement_memories_first, gs->judgement_memories_last, new_forever);
}
if(from->npc_kind == NPC_Daniel)
{
if(gs->judgement_gen_request == 0 && gs->judgement_memories_first)
{
String8List history_list = {0};
for(Memory *it = gs->judgement_memories_first; it; it = it->next)
{
String8List desc = memory_description(scratch.arena, gs->world_entity, it);
S8ListConcat(&history_list, &desc);
}
String8 current_history = S8ListJoin(scratch.arena, history_list, &(StringJoin){0});
Log("Submitting judgement with current history: ```\n%.*s\n```\n", S8VArg(current_history));
String8List current_list = {0};
S8ListPush(scratch.arena, &current_list, make_json_node(scratch.arena, MSG_SYSTEM, S8CString(judgement_system_prompt)));
S8ListPush(scratch.arena, &current_list, make_json_node(scratch.arena, MSG_USER, S8CString(judgement_yes_example)));
S8ListPush(scratch.arena, &current_list, make_json_node(scratch.arena, MSG_ASSISTANT, S8Lit("yes")));
S8ListPush(scratch.arena, &current_list, make_json_node(scratch.arena, MSG_USER, S8CString(judgement_no_example)));
S8ListPush(scratch.arena, &current_list, make_json_node(scratch.arena, MSG_ASSISTANT, S8Lit("no")));
S8ListPush(scratch.arena, &current_list, make_json_node(scratch.arena, MSG_USER, S8CString(judgement_no2_example)));
S8ListPush(scratch.arena, &current_list, make_json_node(scratch.arena, MSG_ASSISTANT, S8Lit("no")));
S8ListPush(scratch.arena, &current_list, make_json_node(scratch.arena, MSG_USER, current_history));
String8 json_array = S8ListJoin(scratch.arena, current_list, &(StringJoin){0});
json_array.size -= 1; // remove trailing comma. fuck json
String8 prompt = FmtWithLint(scratch.arena, "[%.*s]", S8VArg(json_array));
gs->judgement_gen_request = make_generation_request(prompt);
}
}
ReleaseScratch(scratch);
return proceed_propagating;
}
@ -2303,11 +2226,6 @@ void transition_to_room(GameState *gs, ThreeDeeLevel *level, String8 new_room_na
(void)level;
gs->player->current_room_name = new_room_name;
if(S8Match(new_room_name, S8Lit("StartingRoom"), 0))
{
gs->angel->perceptions_dirty = true;
}
}
@ -2336,28 +2254,12 @@ void initialize_gamestate_from_threedee_level(GameState *gs, ThreeDeeLevel *leve
assert(!gs->player);
gs->player = cur_entity;
}
if (cur_entity->npc_kind == NPC_Angel)
{
assert(!gs->angel);
gs->angel = cur_entity;
}
}
}
assert(gs->player);
assert(gs->angel);
gs->world_entity = new_entity(gs);
gs->world_entity->is_world = true;
ENTITIES_ITER(gs->entities)
{
it->rotation = PI32;
it->target_rotation = it->rotation;
}
transition_to_room(gs, &level_threedee, S8Lit("Forest")); // hack to disable cold opening angel sequence right now
// @Place(parse and enact the drama document parse drama)
if(1)
{
@ -2508,7 +2410,6 @@ void initialize_gamestate_from_threedee_level(GameState *gs, ThreeDeeLevel *leve
}
}
}
//Log("Propagated to %d name '%s'...\n", want, characters[want].name);
}
}
@ -2874,23 +2775,14 @@ void end_text_input(char *what_player_said_cstr)
chunk_from_s8(&to_perform.speech, what_player_said);
if(!gs.no_angel_screen)
if (gete(gs.player->talking_to))
{
Entity *angel = 0;
ENTITIES_ITER(gs.entities) if(it->npc_kind == NPC_Angel) angel = it;
to_perform.talking_to_kind = NPC_Angel;
perform_action(&gs, gs.player, to_perform);
assert(gete(gs.player->talking_to)->is_npc);
to_perform.talking_to_kind = gete(gs.player->talking_to)->npc_kind;
}
else
{
if (gete(gs.player->talking_to))
{
assert(gete(gs.player->talking_to)->is_npc);
to_perform.talking_to_kind = gete(gs.player->talking_to)->npc_kind;
}
perform_action(&gs, gs.player, to_perform);
}
perform_action(&gs, gs.player, to_perform);
}
ReleaseScratch(scratch);
}
@ -3484,7 +3376,6 @@ String8 make_devtools_help(Arena *arena)
P("9 - instantly wins the game\n");
P("P - toggles spall profiling on/off, don't leave on for very long as it consumes a lot of storage if you do that. The resulting spall trace is saved to the file '%s'\n", PROFILING_SAVE_FILENAME);
P("If you hover over somebody it will display some parts of their memories, can be somewhat helpful\n");
P("P - immediately kills %s\n", characters[NPC_Raphael].name);
P("J - judges the player and outputs their verdict to the console\n");
#undef P
@ -6107,124 +5998,7 @@ void frame(void)
sg_begin_default_pass(&state.clear_depth_buffer_pass_action, sapp_width(), sapp_height());
sg_apply_pipeline(state.twodee_pip);
// @Place(high priority UI rendering, like angel screen)
// angel screen
{
Entity *angel_entity = 0;
ENTITIES_ITER(gs.entities)
{
if (it->is_npc && it->npc_kind == NPC_Angel && S8Match(it->current_room_name, gs.player->current_room_name, 0))
{
assert(!angel_entity);
angel_entity = it;
}
}
gs.no_angel_screen = angel_entity == 0;
static float visible = 1.0f;
bool should_be_visible = !gs.no_angel_screen;
visible = Lerp(visible, unwarped_dt*2.0f, should_be_visible ? 1.0f : 0.0f);
if(should_be_visible) gs.player->talking_to = frome(angel_entity);
draw_quad((DrawParams) {quad_at(V2(0,screen_size().y), screen_size()), IMG(image_white_square), blendalpha(BLACK, visible), .layer = LAYER_ANGEL_SCREEN});
static TextChunk *to_say_chunk = {0};
static double cur_characters = 0;
if(should_be_visible)
{
assert(angel_entity);
String8 new_to_say = {0};
if(angel_entity->undismissed_action)
{
new_to_say = last_said_sentence(angel_entity);
cur_characters = 0;
angel_entity->undismissed_action = false;
}
if(new_to_say.str != 0)
{
if(to_say_chunk == 0)
{
to_say_chunk = PushArray(persistent_arena, TextChunk, 1);
}
chunk_from_s8(to_say_chunk, new_to_say);
}
}
String8List to_say = {0};
if(to_say_chunk != 0) to_say = split_by_word(frame_arena, TextChunkString8(*to_say_chunk));
String8 cur_word = {0};
String8Node *cur_word_node = 0;
double chars_said = cur_characters;
for(String8Node *cur = to_say.first; cur; cur = cur->next)
{
if((int)chars_said < cur->string.size)
{
cur_word = cur->string;
cur_word_node = cur;
break;
}
chars_said -= (double)cur->string.size;
}
if(!cur_word.str && to_say.last)
{
cur_word = to_say.last->string;
cur_word_node = to_say.last;
}
if(cur_word_node)
{
cur_characters += unwarped_dt * ANGEL_CHARACTERS_PER_SEC;
chars_said += unwarped_dt * ANGEL_CHARACTERS_PER_SEC;
if (chars_said > cur_word.size && cur_word_node->next)
{
play_audio(&sound_angel_grunt_0, 1.0f);
}
assert(cur_word_node);
String8Node *prev_next = cur_word_node->next;
cur_word_node->next = 0;
//String8 without_unsaid = S8ListJoin(frame_arena, to_say, &(StringJoin){.mid = S8Lit(" ")});
PlacedWordList placed = place_wrapped_words(frame_arena, to_say, 1.0f, screen_size().x*0.8f, font_for_text_input, JUST_CENTER);
translate_words_by(placed, V2(screen_size().x*0.1f, screen_size().y*0.75f));
for(PlacedWord *cur = placed.first; cur; cur = cur->next)
{
draw_text((TextParams){false, cur->text, cur->lower_left_corner, blendalpha(WHITE, visible), 1.0f, .use_font = &font_for_text_input, .layer = LAYER_ANGEL_SCREEN});
}
//draw_centered_text((TextParams){false, without_unsaid, V2(screen_size().x * 0.5f, screen_size().y * 0.75f), blendalpha(WHITE, visible), 1.0f, .use_font = &font_for_text_input, .layer = LAYER_ANGEL_SCREEN});
cur_word_node->next = prev_next;
}
if(gs.assigned_objective)
{
float mission_font_scale = 1.0f;
String8 mission_text = FmtWithLint(frame_arena, "Your mission: %.*s", S8VArg(TextChunkString8(gs.objective.description)));
float button_height = 100.0f;
float vert = button_height*0.5f;
draw_centered_text((TextParams){false, mission_text, V2(screen_size().x * 0.5f, screen_size().y * 0.25f + vert), blendalpha(WHITE, visible), mission_font_scale, .use_font = &font_for_text_input, .layer = LAYER_ANGEL_SCREEN});
if(imbutton(aabb_centered(V2(screen_size().x/2.0f, screen_size().y*0.25f - vert), MulV2F(V2(170.0f, button_height), visible)), visible, S8Lit("Accept"), .layer = LAYER_ANGEL_SCREEN, .font = &font_for_text_input))
{
transition_to_room(&gs, &level_threedee, S8Lit("Forest"));
}
}
else
{
draw_centered_text((TextParams){false, S8Lit("(Press E to speak)"), V2(screen_size().x * 0.5f, screen_size().y * 0.25f), blendalpha(WHITE, visible * 0.5f), 0.8f, .use_font = &font_for_text_input, .layer = LAYER_ANGEL_SCREEN});
}
if(should_be_visible && pressed.interact && !gs.assigned_objective)
{
begin_text_input();
pressed.interact = false;
}
if(!gs.no_angel_screen)
{
pressed = (PressedState){0};
}
}
// @Place(high priority UI rendering)
// @Place(text input drawing)
#ifdef DESKTOP
@ -6952,7 +6726,6 @@ void frame(void)
{
bool doesnt_prompt_on_dirty_perceptions = false
|| it->npc_kind == NPC_Player
|| (it->npc_kind == NPC_Angel && gs.no_angel_screen)
|| !npc_does_dialog(it) // not sure what's up with this actually, potentially remove
|| !S8Match(it->current_room_name, gs.player->current_room_name, 0)
|| it->npc_kind == NPC_AngelTotem
@ -7005,8 +6778,7 @@ void frame(void)
"Repeated amounts of testing dialog overwhelmingly in support of the mulaney brothers",
};
char *next_dialog = rigged_dialog[it->times_talked_to % ARRLEN(rigged_dialog)];
char *target = characters[it->memories_last->context.author_npc_kind].name;
target = characters[NPC_Player].name;
String8 target = TextChunkString8(npc_data(gs, it->memories_last->context.author_npc_kind)->name);
ai_response = FmtWithLint(frame_arena, "{\"target\": \"%s\", \"action\": \"%s\", \"action_argument\": \"%s\", \"speech\": \"%s\"}", target, action, action_argument, next_dialog);
it->times_talked_to += 1;
}
@ -7105,7 +6877,6 @@ void frame(void)
float speed = 0.0f;
if(!gs.player->killed) speed = PLAYER_SPEED;
if(!gs.no_angel_screen) speed = 0.0;
// velocity processing for player player movement
{
gs.player->last_moved = NormV2(movement);
@ -7383,29 +7154,29 @@ void frame(void)
Vec2 start_at = V2(0,300);
Vec2 cur_pos = start_at;
AABB bounds = draw_text((TextParams){false, S8Fmt(frame_arena, "--Memories for %s--", characters[to_view->npc_kind].name), cur_pos, WHITE, 1.0});
AABB bounds = draw_text((TextParams){false, S8Fmt(frame_arena, "--Memories for %.*s--", TextChunkVArg(npc_data(gs, to_view->npc_kind)->name)), cur_pos, WHITE, 1.0});
cur_pos.y -= aabb_size(bounds).y;
for(Memory *cur = to_view->memories_first; cur; cur = cur->next)
if(cur->speech.text_length > 0)
{
String8 to_text = cur->context.talking_to_kind != NPC_nobody ? S8Fmt(frame_arena, " to %s ", characters[cur->context.talking_to_kind].name) : S8Lit("");
String8 text = S8Fmt(frame_arena, "%s%s%.*s: %.*s", to_view->npc_kind == cur->context.author_npc_kind ? "(Me) " : "", characters[cur->context.author_npc_kind].name, S8VArg(to_text), cur->speech.text_length, cur->speech);
String8 to_text = cur->context.talking_to_kind != NPC_nobody ? S8Fmt(frame_arena, " to %.*s ", TextChunkVArg(npc_data(gs, cur->context.talking_to_kind)->name)) : S8Lit("");
String8 text = S8Fmt(frame_arena, "%s%.*s%.*s: %.*s", to_view->npc_kind == cur->context.author_npc_kind ? "(Me) " : "", TextChunkVArg(npc_data(gs, cur->context.author_npc_kind)->name), S8VArg(to_text), cur->speech.text_length, cur->speech);
AABB bounds = draw_text((TextParams){false, text, cur_pos, WHITE, 1.0});
cur_pos.y -= aabb_size(bounds).y;
}
if(keypressed[SAPP_KEYCODE_Q] && !receiving_text_input)
{
Log("\n\n==========------- Printing debugging information for %s -------==========\n", characters[to_view->npc_kind].name);
Log("\n\n==========------- Printing debugging information for %.*s -------==========\n", TextChunkVArg(npc_data(gs, to_view->npc_kind)->name));
Log("\nMemories-----------------------------\n");
int mem_idx = 0;
for(Memory *cur = to_view->memories_first; cur; cur = cur->next)
{
String8 to_text = cur->context.talking_to_kind != NPC_nobody ? S8Fmt(frame_arena, " to %s ", characters[cur->context.talking_to_kind].name) : S8Lit("");
String8 to_text = cur->context.talking_to_kind != NPC_nobody ? S8Fmt(frame_arena, " to %.*s ", TextChunkVArg(npc_data(gs, cur->context.talking_to_kind)->name)) : S8Lit("");
String8 speech = TextChunkString8(cur->speech);
if(speech.size == 0) speech = S8Lit("<said nothing>");
String8 text = S8Fmt(frame_arena, "%s%s%.*s: %.*s", to_view->npc_kind == cur->context.author_npc_kind ? "(Me) " : "", characters[cur->context.author_npc_kind].name, S8VArg(to_text), S8VArg(speech));
String8 text = S8Fmt(frame_arena, "%s%.*s%.*s: %.*s", to_view->npc_kind == cur->context.author_npc_kind ? "(Me) " : "", TextChunkVArg(npc_data(gs, cur->context.author_npc_kind)->name), S8VArg(to_text), S8VArg(speech));
printf("Memory %d: %.*s\n", mem_idx, S8VArg(text));
mem_idx++;
}

@ -94,22 +94,20 @@ typedef struct TextChunkList
TextChunk text;
} TextChunkList;
typedef struct GameplayObjective
{
TextChunk description;
} GameplayObjective;
typedef enum
{
ARG_CHARACTER,
ARG_OBJECTIVE,
} ActionArgumentKind;
// A value of 0 means no npc. So is invalid if you're referring to somebody.
typedef int NpcKind;
#define NPC_nobody 0
typedef struct
{
ActionArgumentKind kind;
NpcKind targeting;
GameplayObjective objective;
} ActionArgument;
@ -129,8 +127,6 @@ typedef struct
NpcKind author_npc_kind;
NpcKind talking_to_kind;
bool heard_physically; // if not physically, the source was directly
bool drama_memory; // drama memories arent forgotten, and once they end it's understood that a lot of time has passed.
bool judgement_memory; // judgement memories have special printing for when Daniel says nothing, to make sure the AI understands his attitude towards the player
} MemoryContext;
// memories are subjective to an individual NPC
@ -147,6 +143,20 @@ typedef struct Memory
TextChunk speech;
} Memory;
// text chunk must be a literal, not a pointer
// and this returns a s8 that points at the text chunk memory
#define TextChunkString8(t) S8((u8*)(t).text, (t).text_length)
#define TextChunkVArg(t) S8VArg(TextChunkString8(t))
#define TextChunkLitC(s) {.text = s, .text_length = sizeof(s)}
#define TextChunkLit(s) (TextChunk) TextChunkLitC(s)
void chunk_from_s8(TextChunk *into, String8 from)
{
assert(from.size < ARRLEN(into->text));
memset(into->text, 0, ARRLEN(into->text));
memcpy(into->text, from.str, from.size);
into->text_length = (int)from.size;
}
typedef enum PropKind
{
TREE0,
@ -155,6 +165,7 @@ typedef enum PropKind
ROCK0,
} PropKind;
typedef struct EntityRef
{
int index;
@ -194,34 +205,6 @@ typedef struct
} Target;
// text chunk must be a literal, not a pointer
// and this returns a s8 that points at the text chunk memory
#define TextChunkString8(t) S8((u8*)(t).text, (t).text_length)
#define TextChunkVArg(t) S8VArg(TextChunkString8(t))
void chunk_from_s8(TextChunk *into, String8 from)
{
assert(from.size < ARRLEN(into->text));
memset(into->text, 0, ARRLEN(into->text));
memcpy(into->text, from.str, from.size);
into->text_length = (int)from.size;
}
// returns ai understandable, human readable name, on the arena, so not the enum name
String8 action_argument_string(Arena *arena, ActionArgument arg)
{
switch(arg.kind)
{
case ARG_CHARACTER:
return FmtWithLint(arena, "%s", characters[arg.targeting].name);
break;
case ARG_OBJECTIVE:
return FmtWithLint(arena, "%.*s", S8VArg(TextChunkString8(arg.objective.description)));
}
return (String8){0};
}
typedef struct RememberedError
{
struct RememberedError *next;
@ -295,28 +278,27 @@ float entity_max_damage(Entity *e)
typedef BUFF(ActionKind, 8) AvailableActions;
typedef struct Npc {
TextChunk name;
NpcKind kind; // must not be 0, that's nobody!
TextChunk prompt;
} Npc;
typedef struct GameState {
Arena *arena;
Arena *arena; // all allocations done with the lifecycle of a gamestate (loading/reloading entire levels essentially) must be allocated on this arena.
uint64_t tick;
bool won;
bool finished_reading_dying_dialog;
bool no_angel_screen;
bool assigned_objective;
GameplayObjective objective;
Memory *judgement_memories_first;
Memory *judgement_memories_last;
double time; // in seconds, fraction of length of day
int judgement_gen_request;
// processing may still occur after time has stopped on the gamestate,
bool stopped_time;
BUFF(Npc, 10) characters;
// these must point entities in its own array.
Entity *player;
Entity *angel;
Entity *world_entity;
Entity entities[MAX_ENTITIES];
rnd_gamerand_t random;
@ -324,6 +306,57 @@ typedef struct GameState {
#define ENTITIES_ITER(ents) for (Entity *it = ents; it < ents + ARRLEN(ents); it++) if (it->exists && !it->destroy && it->generation > 0)
Npc nobody_data = {
.name = TextChunkLitC("Nobody"),
.kind = NPC_nobody,
};
Npc *npc_data_by_name(GameState *gs, String8 name) {
BUFF_ITER(Npc, &gs->characters) {
if(S8Match(TextChunkString8(it->name), name, 0)) {
return it;
}
}
return 0;
}
Npc *npc_data(GameState *gs, NpcKind kind) {
if(kind == NPC_nobody) return &nobody_data;
BUFF_ITER(Npc, &gs->characters) {
if(it->kind == kind) {
return it;
}
}
Log("Unknown npc kind '%d'\n", kind);
assert(false);
return 0;
}
String8 npc_to_human_readable(GameState *gs, Entity *me, NpcKind kind)
{
if(me->npc_kind == kind)
{
return S8Lit("You");
}
else
{
return TextChunkString8(npc_data(gs, kind)->name);
}
}
// returns ai understandable, human readable name, on the arena, so not the enum name
String8 action_argument_string(Arena *arena, GameState *gs, ActionArgument arg)
{
switch(arg.kind)
{
case ARG_CHARACTER:
return FmtWithLint(arena, "%.*s", TextChunkVArg(npc_data(gs, arg.targeting)->name));
break;
}
return (String8){0};
}
float g_randf(GameState *gs)
{
return rnd_gamerand_nextf(&gs->random);
@ -348,17 +381,6 @@ void fill_available_actions(GameState *gs, Entity *it, AvailableActions *a)
*a = (AvailableActions) { 0 };
BUFF_APPEND(a, ACT_none);
if(it->npc_kind == NPC_Angel)
{
BUFF_APPEND(a, ACT_assign_gameplay_objective);
return;
}
if(it->npc_kind == NPC_Tombstone)
{
return;
}
if(gete_specified(gs, it->joined))
{
BUFF_APPEND(a, ACT_leave)
@ -367,13 +389,7 @@ void fill_available_actions(GameState *gs, Entity *it, AvailableActions *a)
{
BUFF_APPEND(a, ACT_join)
}
if(it->npc_kind != NPC_Angel)
{
BUFF_APPEND(a, ACT_end_conversation);
}
bool has_shotgun = it->npc_kind == NPC_Daniel;
bool has_shotgun = false;
if(has_shotgun)
{
if(gete_specified(gs, it->aiming_shotgun_at))
@ -388,11 +404,12 @@ void fill_available_actions(GameState *gs, Entity *it, AvailableActions *a)
}
}
bool npc_does_dialog(Entity *it)
typedef enum
{
return it->npc_kind < ARRLEN(characters);
}
MSG_SYSTEM,
MSG_USER,
MSG_ASSISTANT,
} MessageType;
// for no trailing comma just trim the last character
String8 make_json_node(Arena *arena, MessageType type, String8 content)
@ -415,20 +432,8 @@ String8 make_json_node(Arena *arena, MessageType type, String8 content)
return to_return;
}
String8 npc_to_human_readable(Entity *me, NpcKind kind)
{
if(me->npc_kind == kind)
{
return S8Lit("You");
}
else
{
return S8CString(characters[kind].name);
}
}
String8List dump_memory_as_json(Arena *arena, Memory *it)
String8List dump_memory_as_json(Arena *arena, GameState *gs, Memory *it)
{
ArenaTemp scratch = GetScratch(&arena, 1);
String8List current_list = {0};
@ -439,28 +444,21 @@ String8List dump_memory_as_json(Arena *arena, Memory *it)
AddFmt("\"action\":\"%s\",", actions[it->action_taken].name);
String8 arg_str = action_argument_string(scratch.arena, it->action_argument);
AddFmt("\"action_argument\":\"%.*s\",", S8VArg(arg_str));
AddFmt("\"target\":\"%s\"}", characters[it->context.talking_to_kind].name);
AddFmt("\"target\":\"%.*s\"}", TextChunkVArg(npc_data(gs, it->context.talking_to_kind)->name));
#undef AddFmt
ReleaseScratch(scratch);
return current_list;
}
String8List memory_description(Arena *arena, Entity *e, Memory *it)
String8List memory_description(Arena *arena, GameState *gs, Entity *e, Memory *it)
{
String8List current_list = {0};
#define AddFmt(...) PushWithLint(arena, &current_list, __VA_ARGS__)
// dump a human understandable sentence description of what happened in this memory
bool no_longer_wants_to_converse = false; // add the no longer wants to converse text after any speech, it makes more sense reading it
#define HUMAN(kind) S8VArg(npc_to_human_readable(e, kind))
if(it->context.judgement_memory)
{
if((it->action_taken == ACT_none && it->speech.text_length == 0) && it->context.author_npc_kind != NPC_Player)
{
AddFmt("%.*s said and did nothing in response\n", HUMAN(it->context.author_npc_kind));
}
}
#define HUMAN(kind) S8VArg(npc_to_human_readable(gs, e, kind))
if (it->action_taken != ACT_none)
{
switch (it->action_taken)
@ -504,7 +502,7 @@ String8List memory_description(Arena *arena, Entity *e, Memory *it)
if (it->context.talking_to_kind == e->npc_kind)
target_string = S8Lit("you");
else
target_string = S8CString(characters[it->context.talking_to_kind].name);
target_string = TextChunkString8(npc_data(gs, it->context.talking_to_kind)->name);
}
if(!e->is_world)
@ -518,10 +516,10 @@ String8List memory_description(Arena *arena, Entity *e, Memory *it)
AddFmt("(Overheard conversation, they aren't speaking directly to you) ");
}
}
AddFmt("%s said \"%.*s\" to %.*s", characters[it->context.author_npc_kind].name, TextChunkVArg(it->speech), S8VArg(target_string));
AddFmt("%.*s said \"%.*s\" to %.*s", TextChunkVArg(npc_data(gs, it->context.author_npc_kind)->name), TextChunkVArg(it->speech), S8VArg(target_string));
if(!e->is_world)
{
AddFmt(" (you are %s)", characters[e->npc_kind].name)
AddFmt(" (you are %.*s)", TextChunkVArg(npc_data(gs, e->npc_kind)->name));
}
AddFmt("\n");
}
@ -546,7 +544,7 @@ String8List memory_description(Arena *arena, Entity *e, Memory *it)
String8 generate_chatgpt_prompt(Arena *arena, GameState *gs, Entity *e, CanTalkTo can_talk_to)
{
assert(e->is_npc);
assert(e->npc_kind < ARRLEN(characters));
assert(npc_data(gs, e->npc_kind) != 0);
ArenaTemp scratch = GetScratch(&arena, 1);
@ -562,7 +560,7 @@ String8 generate_chatgpt_prompt(Arena *arena, GameState *gs, Entity *e, CanTalkT
{
String8List current_list = {0};
AddFmt("%s\n\n", global_prompt);
AddFmt("%s\n\n", characters[e->npc_kind].prompt);
AddFmt("%.*s\n\n", TextChunkVArg(npc_data(gs, e->npc_kind)->prompt));
AddFmt("The characters who are near you, that you can target:\n");
BUFF_ITER(Entity*, &can_talk_to)
{
@ -572,7 +570,7 @@ String8 generate_chatgpt_prompt(Arena *arena, GameState *gs, Entity *e, CanTalkT
{
info = S8Lit(" - they're currently dead, they were murdered");
}
AddFmt("%s%.*s\n", characters[(*it)->npc_kind].name, S8VArg(info));
AddFmt("%.*s%.*s\n", TextChunkVArg(npc_data(gs, (*it)->npc_kind)->name), S8VArg(info));
}
AddFmt("\n");
@ -608,7 +606,7 @@ String8 generate_chatgpt_prompt(Arena *arena, GameState *gs, Entity *e, CanTalkT
in_drama_memories = false;
AddFmt("Some time passed...\n");
}
String8List desc_list = memory_description(scratch.arena, e, it);
String8List desc_list = memory_description(scratch.arena, gs, e, it);
S8ListConcat(&current_list, &desc_list);
}
@ -622,7 +620,7 @@ String8 generate_chatgpt_prompt(Arena *arena, GameState *gs, Entity *e, CanTalkT
{
if(cur->offending_self_output.speech.text_length > 0 || cur->offending_self_output.action_taken != ACT_none)
{
String8 offending_json_output = S8ListJoin(scratch.arena, dump_memory_as_json(scratch.arena, &cur->offending_self_output), &(StringJoin){0});
String8 offending_json_output = S8ListJoin(scratch.arena, dump_memory_as_json(scratch.arena, gs, &cur->offending_self_output), &(StringJoin){0});
AddFmt("When you output, `%.*s`, ", S8VArg(offending_json_output));
}
AddFmt("%.*s\n", TextChunkVArg(cur->reason_why_its_bad));
@ -635,7 +633,7 @@ String8 generate_chatgpt_prompt(Arena *arena, GameState *gs, Entity *e, CanTalkT
if(it->context.i_said_this)
{
String8List current_list = {0}; // shadow the list of human understandable sentences to quickly flush
current_list = dump_memory_as_json(scratch.arena, it);
current_list = dump_memory_as_json(scratch.arena, gs, it);
AddNewNode(MSG_ASSISTANT);
}
}
@ -656,7 +654,7 @@ String8 get_field(Node *parent, String8 name)
return MD_ChildFromString(parent, name, 0)->first_child->string;
}
void parse_action_argument(Arena *error_arena, String8 *cur_error_message, ActionKind action, String8 action_argument_str, ActionArgument *out)
void parse_action_argument(Arena *error_arena, GameState *gs, String8 *cur_error_message, ActionKind action, String8 action_argument_str, ActionArgument *out)
{
assert(cur_error_message);
if(cur_error_message->size > 0) return;
@ -664,38 +662,21 @@ void parse_action_argument(Arena *error_arena, String8 *cur_error_message, Actio
String8 action_str = S8CString(actions[action].name);
// @TODO refactor into, action argument kinds and they parse into different action argument types
bool arg_is_character = action == ACT_join || action == ACT_aim_shotgun || action == ACT_end_conversation;
bool arg_is_gameplay_objective = action == ACT_assign_gameplay_objective;
if (arg_is_character)
{
out->kind = ARG_CHARACTER;
bool found_npc = false;
for (int i = 0; i < ARRLEN(characters); i++)
{
if (S8Match(S8CString(characters[i].name), action_argument_str, 0))
{
found_npc = true;
(*out).targeting = i;
}
Npc * npc = npc_data_by_name(gs, action_argument_str);
found_npc = npc != 0;
if(npc) {
out->targeting = npc->kind;
}
if (!found_npc)
{
*cur_error_message = FmtWithLint(error_arena, "Argument for action `%.*s` you gave is `%.*s`, which doesn't exist in the game so is invalid", S8VArg(action_str), S8VArg(action_argument_str));
}
}
else if (arg_is_gameplay_objective)
{
out->kind = ARG_OBJECTIVE;
if(action_argument_str.size >= MAX_SENTENCE_LENGTH)
{
String8 trimmed = S8Substring(action_argument_str, action_argument_str.size - MAX_SENTENCE_LENGTH/2, action_argument_str.size);
*cur_error_message = FmtWithLint(error_arena, "What you said for your action argument, '%.*s...' is WAY too big for the game to handle, it can be a maximum of %d characters, but you output %d!.", S8VArg(trimmed), MAX_SENTENCE_LENGTH, (int)action_argument_str.size);
}
if(cur_error_message->size == 0)
{
chunk_from_s8(&out->objective.description, action_argument_str);
}
}
else
{
assert(false); // don't know how to parse the argument string for this kind of action...
@ -755,14 +736,20 @@ String8 parse_chatgpt_response(Arena *arena, Entity *e, String8 action_in_json,
}
else
{
Npc *npc_data_by_name(GameState *gs, String8 name) {
BUFF_ITER(Npc, &gs->characters) {
if(S8Match(TextChunkString8(it->name), name, 0)) {
return it;
}
}
return 0;
}
Npc * npc = ncp_data_by_name(gs, target_str);
bool found = false;
for(int i = 0; i < ARRLEN(characters); i++)
{
if(S8Match(target_str, S8CString(characters[i].name), 0))
{
found = true;
out->talking_to_kind = i;
}
if(npc) {
found = true;
out->talking_to_kind = npc->kind;
}
if(!found)
{

@ -12,7 +12,7 @@ if exist gen\ (
thirdparty\sokol-shdc.exe --input threedee.glsl --output gen\threedee.glsl.h --slang glsl300es:hlsl5:glsl330 || goto :error
@REM metadesk codegen
cl /nologo /diagnostics:caret /Ithirdparty /W3 /Zi /WX codegen.c || goto :error
cl /nologo /diagnostics:caret /Ithirdparty /FC /W3 /Zi /WX codegen.c || goto :error
@REM zig cc -Ithirdparty -gfull -gcodeview codegen.c -o codegen.exe || goto error
codegen || goto :error

@ -10,7 +10,6 @@
#define PERCEPTION_HEARING_RAGE (TILE_SIZE*4.0f)
#define CHARACTERS_PER_SEC 45.0f
#define ANGEL_CHARACTERS_PER_SEC 35.0f
#define PROPAGATE_ACTIONS_RADIUS 4.0f
#define SWORD_SWIPE_RADIUS (TILE_SIZE*3.0f)
#define ARROW_SPEED 200.0f
#define SECONDS_PER_ARROW 1.3f

Loading…
Cancel
Save