NPCs can select who their actions and speech are directed at

main
Cameron Murphy Reikes 2 years ago
parent dfb4e06fff
commit 1a38114f59

@ -1,6 +1,6 @@
[ [
{can_hear: [NPC_Jester, NPC_Bill]}, {can_hear: [NPC_Jester, NPC_Bill]},
{enum: NPC_Jester , dialog: "Hehehe! Quit quant, a peasant from the mortal realm!"}, {enum: NPC_Jester , dialog: "Hehehe! Quit quant, a peasant from the mortal realm!", to: Bill},
{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_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_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_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"},

@ -5,8 +5,9 @@
// @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. // @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 colorful and interesting personality in an RPG video game, who remembers important memories from the conversation history and stays in character.\n" const char *global_prompt = "You are a colorful and interesting personality in an RPG video game, who remembers important memories from the conversation history and stays in character.\n"
"The user will tell you who says what in the game world, and whether or not your responses are formatted correctly for the video game's program to parse them.\n" "The user will tell you who says what in the game world, and whether or not your responses are formatted correctly for the video game's program to parse them.\n"
"Messages are json-like dictionaries that look like this: `{action: your_action, speech: \"Hey player!\", thoughts: \"Your thoughts\"}`. The required fields are `action` and `thoughts`\n" "Messages are json-like dictionaries that look like this: `{who_i_am: who you're acting as, talking_to: who this action is directed at, could be nobody, action: your_action, speech: \"Hey player!\", thoughts: \"Your thoughts\"}`. The required fields are `action`, `thoughts`, and `who_i_am`\n"
"Some actions take an argument, which you can provide with the field `action_arg`, e.g for the action `give_item` you would provide an item in your inventory, like {action: give_item, action_arg: Chalice}. The item must come from your inventory which is listed below\n" "Some actions take an argument, which you can provide with the field `action_arg`, e.g for the action `give_item` you would provide an item in your inventory, like {action: give_item, action_arg: Chalice}. The item must come from your inventory which is listed below\n"
"If was_heard_in_passing is true, then the action wasn't said directly to you, it was heard physically close to you. So, you usually respond with action: none and omit the speech field, as something you hear around the corner or whatever you don't normally respond to unless you feel the need to interrupt or something like that.\n"
"Do NOT give away an item until the player gives you something you think is of equal value\n" "Do NOT give away an item until the player gives you something you think is of equal value\n"
; ;

350
main.c

@ -119,7 +119,6 @@ void web_arena_set_auto_align(WebArena *arena, size_t align)
#include "md.c" #include "md.c"
#pragma warning(pop) #pragma warning(pop)
MD_Arena *persistent_arena = 0; // watch out, arenas have limited size.
#include <math.h> #include <math.h>
@ -255,7 +254,7 @@ void do_parsing_tests()
speech = MD_S8Lit("Better have a good reason for bothering me."); speech = MD_S8Lit("Better have a good reason for bothering me.");
MD_String8 thoughts = MD_S8Lit("Man I'm tired today Whatever."); MD_String8 thoughts = MD_S8Lit("Man I'm tired today Whatever.");
MD_String8 to_parse = FmtWithLint(scratch.arena, "{action: none, speech: \"%.*s\", thoughts: \"%.*s\"}", MD_S8VArg(speech), MD_S8VArg(thoughts)); MD_String8 to_parse = FmtWithLint(scratch.arena, "{action: none, speech: \"%.*s\", thoughts: \"%.*s\", who_i_am: TheBlacksmith, talking_to: nobody}", MD_S8VArg(speech), MD_S8VArg(thoughts));
error = parse_chatgpt_response(scratch.arena, &e, to_parse, &a); error = parse_chatgpt_response(scratch.arena, &e, to_parse, &a);
assert(error.size == 0); assert(error.size == 0);
assert(a.kind == ACT_none); assert(a.kind == ACT_none);
@ -273,7 +272,7 @@ void do_parsing_tests()
error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(Chalice \""), &a); error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(Chalice \""), &a);
assert(error.size > 0); assert(error.size > 0);
to_parse = MD_S8Lit("{action: give_item, action_arg: Chalice, speech: \"Here you go\", thoughts: \"Man I'm gonna miss that chalice\"}"); to_parse = MD_S8Lit("{action: give_item, action_arg: Chalice, speech: \"Here you go\", thoughts: \"Man I'm gonna miss that chalice\", who_i_am: TheBlacksmith, talking_to: nobody}");
error = parse_chatgpt_response(scratch.arena, &e, to_parse, &a); error = parse_chatgpt_response(scratch.arena, &e, to_parse, &a);
assert(error.size == 0); assert(error.size == 0);
assert(a.kind == ACT_give_item); assert(a.kind == ACT_give_item);
@ -423,6 +422,7 @@ Vec2 FloorV2(Vec2 v)
} }
MD_Arena *frame_arena = 0; MD_Arena *frame_arena = 0;
MD_Arena *persistent_arena = 0; // watch out, arenas have limited size.
#ifdef WINDOWS #ifdef WINDOWS
// uses frame arena // uses frame arena
@ -962,6 +962,31 @@ void push_memory(Entity *e, MD_String8 speech, MD_String8 monologue, ActionKind
} }
} }
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)
{
BUFF_APPEND(&to_return, it->npc_kind);
}
}
return to_return;
}
Entity *get_targeted(Entity *from, NpcKind targeted)
{
ENTITIES_ITER(gs.entities)
{
if(it != from && it->is_npc && LenV2(SubV2(it->pos, from->pos)) < PROPAGATE_ACTIONS_RADIUS && it->npc_kind == targeted)
{
return it;
}
}
return 0;
}
void remember_error(Entity *to_modify, MD_String8 error_message) void remember_error(Entity *to_modify, MD_String8 error_message)
{ {
assert(!to_modify->is_character); // this is a game logic bug if a player action is invalid assert(!to_modify->is_character); // this is a game logic bug if a player action is invalid
@ -979,20 +1004,87 @@ void remember_action(Entity *to_modify, Action a, MemoryContext context)
} }
} }
// from must not be null, to can be null if the action isn't directed at anybody // returns reason why allocated on arena if invalid
// to might be null here, from can't be null
MD_String8 is_action_valid(MD_Arena *arena, Entity *from, Action a)
{
assert(a.speech_length <= MAX_SENTENCE_LENGTH && a.speech_length >= 0);
assert(a.kind >= 0 && a.kind < ARRLEN(actions));
assert(from);
CanTalkTo talk = get_can_talk_to(from);
if(a.talking_to_somebody)
{
bool found = false;
BUFF_ITER(NpcKind, &talk)
{
if(*it == a.talking_to_kind)
{
found = true;
break;
}
}
if(!found)
{
return FmtWithLint(arena, "Character you're talking to, %s, isn't close enough to be talked to", characters[a.talking_to_kind].enum_name);
}
}
if(a.kind == ACT_give_item)
{
assert(a.argument.item_to_give >= 0 && a.argument.item_to_give < ARRLEN(items));
bool has_it = false;
BUFF_ITER(ItemKind, &from->held_items)
{
if(*it == a.argument.item_to_give)
{
has_it = true;
break;
}
}
if(!has_it)
{
MD_StringJoin join = {.mid = MD_S8Lit(", ")};
return FmtWithLint(arena, "Can't give item `ITEM_%s`, you only have [%.*s] in your inventory", items[a.argument.item_to_give].enum_name, MD_S8VArg(MD_S8ListJoin(arena, held_item_strings(arena, from), &join)));
}
if(!a.talking_to_somebody)
{
return MD_S8Lit("You can't give an item to nobody, must target somebody to give an item");
}
}
if(a.kind == ACT_leaves_player && from->standing != STANDING_JOINED)
{
return MD_S8Lit("You can't leave the player unless you joined them.");
}
return (MD_String8){0};
}
// from must not be null
// the action must have been validated to be valid if you're calling this // the action must have been validated to be valid if you're calling this
void cause_action_side_effects(Entity *from, Entity *to, Action a) void cause_action_side_effects(Entity *from, Action a)
{ {
assert(from); assert(from);
MD_ArenaTemp scratch = MD_GetScratch(0, 0); MD_ArenaTemp scratch = MD_GetScratch(0, 0);
MD_String8 failure_reason = is_action_valid(scratch.arena, from, to, a);
MD_String8 failure_reason = is_action_valid(scratch.arena, from, a);
if(failure_reason.size > 0) if(failure_reason.size > 0)
{ {
Log("Failed to process action, invalid action: `%.*s`\n", MD_S8VArg(failure_reason)); Log("Failed to process action, invalid action: `%.*s`\n", MD_S8VArg(failure_reason));
assert(false); assert(false);
} }
Entity *to = 0;
if(a.talking_to_somebody)
{
to = get_targeted(from, a.talking_to_kind);
assert(to);
}
if(a.kind == ACT_give_item) if(a.kind == ACT_give_item)
{ {
assert(a.argument.item_to_give != ITEM_invalid); assert(a.argument.item_to_give != ITEM_invalid);
@ -1037,6 +1129,56 @@ void cause_action_side_effects(Entity *from, Entity *to, Action a)
MD_ReleaseScratch(scratch); MD_ReleaseScratch(scratch);
} }
typedef struct PropagatingAction
{
struct PropagatingAction *next;
Action a;
MemoryContext context;
Vec2 from;
bool already_propagated_to[MAX_ENTITIES]; // tracks by index of entity
float progress; // if greater than or equal to 1.0, is freed
} PropagatingAction;
PropagatingAction *propagating = 0;
PropagatingAction ignore_entity(Entity *to_ignore, PropagatingAction p)
{
PropagatingAction to_return = p;
to_return.already_propagated_to[frome(to_ignore).index] = true;
return to_return;
}
void push_propagating(PropagatingAction to_push)
{
to_push.context.heard_physically = true;
bool found = false;
for(PropagatingAction *cur = propagating; cur; cur = cur->next)
{
if(cur->progress >= 1.0f)
{
PropagatingAction *prev_next = cur->next;
*cur = to_push;
cur->next = prev_next;
found = true;
break;
}
}
if(!found)
{
PropagatingAction *cur = MD_PushArray(persistent_arena, PropagatingAction, 1);
*cur = to_push;
MD_StackPush(propagating, cur);
}
}
float propagating_radius(PropagatingAction *p)
{
float t = powf(p->progress, 0.65f);
return Lerp(0.0f, t, PROPAGATE_ACTIONS_RADIUS );
}
// only called when the action is instantiated, correctly propagates the information // only called when the action is instantiated, correctly propagates the information
// of the action physically and through the party // of the action physically and through the party
// If the action is invalid, remembers the error if it's an NPC, and does nothing else // If the action is invalid, remembers the error if it's an NPC, and does nothing else
@ -1048,22 +1190,18 @@ bool perform_action(Entity *from, Action a)
MemoryContext context = {0}; MemoryContext context = {0};
context.author_npc_kind = from->npc_kind; context.author_npc_kind = from->npc_kind;
Entity *action_target = 0;
if(from == player && gete(from->talking_to)) if(from == player && gete(from->talking_to))
{ {
action_target = gete(from->talking_to); context.was_talking_to_somebody = true;
assert(action_target->is_npc); context.talking_to_kind = gete(from->talking_to)->npc_kind;
} }
else if(gete(player->talking_to) == from) else
action_target = player;
if(action_target)
{ {
context.was_directed_at_somebody = true; context.was_talking_to_somebody = a.talking_to_somebody;
context.directed_at_kind = action_target->npc_kind; context.talking_to_kind = a.talking_to_kind;
} }
MD_String8 is_valid = is_action_valid(scratch.arena, from, action_target, a); MD_String8 is_valid = is_action_valid(scratch.arena, from, a);
bool proceed_propagating = true; bool proceed_propagating = true;
if(is_valid.size > 0) if(is_valid.size > 0)
{ {
@ -1078,7 +1216,9 @@ bool perform_action(Entity *from, Action a)
if(proceed_propagating) if(proceed_propagating)
{ {
cause_action_side_effects(from, action_target, a); Entity *targeted = get_targeted(from, a.talking_to_kind);
cause_action_side_effects(from, a);
// self memory // self memory
if(!from->is_character) if(!from->is_character)
{ {
@ -1088,46 +1228,22 @@ bool perform_action(Entity *from, Action a)
} }
// memory of target // memory of target
if(action_target) if(targeted)
{
remember_action(action_target, a, context);
}
bool propagate_to_party = from->is_character || (from->is_npc && from->standing == STANDING_JOINED);
if(action_target == player) propagate_to_party = true;
if(context.eavesdropped_from_party) propagate_to_party = false;
if(propagate_to_party)
{ {
ENTITIES_ITER(gs.entities) remember_action(targeted, a, context);
{
if(it->is_npc && it->standing == STANDING_JOINED && it != from && it != action_target)
{
MemoryContext eavesdropped_context = context;
eavesdropped_context.eavesdropped_from_party = true;
remember_action(it, a, eavesdropped_context);
}
}
} }
// propagate physically
// npcs in party when they talk should have their speech heard by who the player is talking to PropagatingAction to_propagate = {0};
if(from->is_npc && from->standing == STANDING_JOINED) to_propagate.a = a;
to_propagate.context = context;
to_propagate.from = from->pos;
to_propagate = ignore_entity(from, to_propagate);
if(targeted)
{ {
if(gete(player->talking_to) && gete(player->talking_to) != from) to_propagate = ignore_entity(targeted, to_propagate);
{
assert(gete(player->talking_to));
assert(gete(player->talking_to)->is_npc);
MemoryContext from_party_context = context;
from_party_context.directed_at_kind = gete(player->talking_to)->npc_kind;
remember_action(gete(player->talking_to), a, from_party_context);
}
} }
push_propagating(to_propagate);
// TODO Propagate physically
} }
MD_ReleaseScratch(scratch); MD_ReleaseScratch(scratch);
@ -1303,9 +1419,13 @@ void reset_level()
assert(ARRLEN(to_load->initial_entities) == ARRLEN(gs.entities)); assert(ARRLEN(to_load->initial_entities) == ARRLEN(gs.entities));
memcpy(gs.entities, to_load->initial_entities, sizeof(Entity) * MAX_ENTITIES); memcpy(gs.entities, to_load->initial_entities, sizeof(Entity) * MAX_ENTITIES);
gs.version = CURRENT_VERSION; gs.version = CURRENT_VERSION;
ENTITIES_ITER(gs.entities)
for (Entity *it = gs.entities; it < gs.entities + ARRLEN(gs.entities); it++)
{ {
if (it->generation == 0) it->generation = 1; // zero value generation means doesn't exist if(it->exists && it->generation == 0)
{
it->generation = 1;
}
} }
} }
update_player_from_entities(); update_player_from_entities();
@ -2563,6 +2683,7 @@ void draw_animated_sprite(DrawnAnimatedSprite d)
draw_quad(drawn); draw_quad(drawn);
} }
// gets aabbs overlapping the input aabb, including gs.entities and tiles // gets aabbs overlapping the input aabb, including gs.entities and tiles
Overlapping get_overlapping(Level *l, AABB aabb) Overlapping get_overlapping(Level *l, AABB aabb)
{ {
@ -2888,7 +3009,6 @@ typedef struct
MD_u8 speech[MAX_SENTENCE_LENGTH]; MD_u8 speech[MAX_SENTENCE_LENGTH];
int speech_length; int speech_length;
DialogElementKind kind; DialogElementKind kind;
bool was_eavesdropped;
NpcKind who_said_it; NpcKind who_said_it;
bool was_last_said; bool was_last_said;
} DialogElement; } DialogElement;
@ -2921,7 +3041,7 @@ MD_String8List last_said_without_unsaid_words(MD_Arena *arena, Entity *it)
// and an argument. So worst case every perception has 2 dialog // and an argument. So worst case every perception has 2 dialog
// elements right now is why it's *2 // elements right now is why it's *2
typedef BUFF(DialogElement, REMEMBERED_MEMORIES*2) Dialog; typedef BUFF(DialogElement, REMEMBERED_MEMORIES*2) Dialog;
Dialog produce_dialog(Entity *talking_to, bool character_names) Dialog get_dialog_elems(Entity *talking_to, bool character_names)
{ {
MD_ArenaTemp scratch = MD_GetScratch(0, 0); MD_ArenaTemp scratch = MD_GetScratch(0, 0);
assert(talking_to->is_npc); assert(talking_to->is_npc);
@ -2932,7 +3052,7 @@ Dialog produce_dialog(Entity *talking_to, bool character_names)
{ {
if(it->speech_length > 0) if(it->speech_length > 0)
{ {
DialogElement new_element = { .who_said_it = it->context.author_npc_kind, .was_eavesdropped = it->context.eavesdropped_from_party }; DialogElement new_element = { .who_said_it = it->context.author_npc_kind };
MD_String8 my_speech = MD_S8(it->speech, it->speech_length); MD_String8 my_speech = MD_S8(it->speech, it->speech_length);
if(last_said_sentence(talking_to).str == it->speech) if(last_said_sentence(talking_to).str == it->speech)
@ -2941,7 +3061,17 @@ Dialog produce_dialog(Entity *talking_to, bool character_names)
my_speech = MD_S8ListJoin(scratch.arena, last_said_without_unsaid_words(scratch.arena, talking_to), &(MD_StringJoin){.mid = MD_S8Lit(" ")}); my_speech = MD_S8ListJoin(scratch.arena, last_said_without_unsaid_words(scratch.arena, talking_to), &(MD_StringJoin){.mid = MD_S8Lit(" ")});
} }
MD_String8 dialog_speech = FmtWithLint(scratch.arena, "%s: %.*s", characters[it->context.author_npc_kind].name, MD_S8VArg(my_speech)); MD_String8 name_string = {0};
if(it->context.was_talking_to_somebody)
{
name_string = FmtWithLint(scratch.arena, "%s to %s", characters[it->context.author_npc_kind].name, characters[it->context.talking_to_kind].name);
}
else
{
name_string = FmtWithLint(scratch.arena, "%s", characters[it->context.author_npc_kind].name);
}
MD_String8 dialog_speech = FmtWithLint(scratch.arena, "%.*s: %.*s", MD_S8VArg(name_string), MD_S8VArg(my_speech));
memcpy(new_element.speech, dialog_speech.str, dialog_speech.size); memcpy(new_element.speech, dialog_speech.str, dialog_speech.size);
new_element.speech_length = (int)dialog_speech.size; new_element.speech_length = (int)dialog_speech.size;
@ -3058,7 +3188,7 @@ void draw_dialog_panel(Entity *talking_to, float alpha)
{ {
float new_line_height = dialog_panel.lower_right.Y; float new_line_height = dialog_panel.lower_right.Y;
Dialog dialog = produce_dialog(talking_to, false); Dialog dialog = get_dialog_elems(talking_to, false);
if (dialog.cur_index > 0) if (dialog.cur_index > 0)
{ {
for (int i = dialog.cur_index - 1; i >= 0; i--) for (int i = dialog.cur_index - 1; i >= 0; i--)
@ -3068,28 +3198,21 @@ void draw_dialog_panel(Entity *talking_to, float alpha)
Color color; Color color;
// decide color // decide color
{ {
if(it->was_eavesdropped) if (it->kind == DELEM_PLAYER)
{ {
color = colhex(0x9341a3); color = BLACK;
}
else if (it->kind == DELEM_NPC)
{
color = colhex(0x345e22);
}
else if (it->kind == DELEM_ACTION_DESCRIPTION)
{
color = colhex(0xb5910e);
} }
else else
{ {
if (it->kind == DELEM_PLAYER) assert(false);
{
color = BLACK;
}
else if (it->kind == DELEM_NPC)
{
color = colhex(0x345e22);
}
else if (it->kind == DELEM_ACTION_DESCRIPTION)
{
color = colhex(0xb5910e);
}
else
{
assert(false);
}
} }
} }
@ -3408,6 +3531,29 @@ void frame(void)
gs.tick += 1; gs.tick += 1;
PROFILE_SCOPE("propagate actions")
{
for(PropagatingAction *cur = propagating; cur; cur = cur->next)
{
if(cur->progress < 1.0f)
{
cur->progress += dt;
float effective_radius = propagating_radius(cur);
ENTITIES_ITER(gs.entities)
{
if(it->is_npc && LenV2(SubV2(it->pos, cur->from)) < effective_radius)
{
if(!cur->already_propagated_to[frome(it).index])
{
cur->already_propagated_to[frome(it).index] = true;
remember_action(it, cur->a, cur->context);
}
}
}
}
}
}
// process gs.entities // process gs.entities
player_in_combat = false; // in combat set by various enemies when they fight the player player_in_combat = false; // in combat set by various enemies when they fight the player
PROFILE_SCOPE("entity processing") PROFILE_SCOPE("entity processing")
@ -3988,7 +4134,7 @@ void frame(void)
it->perceptions_dirty = false; // needs to be in beginning because they might be redirtied by the new perception it->perceptions_dirty = false; // needs to be in beginning because they might be redirtied by the new perception
MD_String8 prompt_str = {0}; MD_String8 prompt_str = {0};
#ifdef DO_CHATGPT_PARSING #ifdef DO_CHATGPT_PARSING
prompt_str = generate_chatgpt_prompt(frame_arena, it); prompt_str = generate_chatgpt_prompt(frame_arena, it, get_can_talk_to(it));
#else #else
generate_prompt(it, &prompt); generate_prompt(it, &prompt);
#endif #endif
@ -4026,10 +4172,6 @@ void frame(void)
ActionKind act = ACT_none; ActionKind act = ACT_none;
it->times_talked_to++; it->times_talked_to++;
if(it->memories.data[it->memories.cur_index-1].context.eavesdropped_from_party)
{
PushWithLint(scratch.arena, &dialog_elems, "Responding to eavesdropped: ");
}
if(it->npc_kind == NPC_TheBlacksmith && it->standing != STANDING_JOINED) if(it->npc_kind == NPC_TheBlacksmith && it->standing != STANDING_JOINED)
{ {
assert(it->times_talked_to == 1); assert(it->times_talked_to == 1);
@ -4524,6 +4666,19 @@ void frame(void)
} }
} }
PROFILE_SCOPE("propagating")
{
for(PropagatingAction *cur = propagating; cur; cur = cur->next)
{
if(cur->progress < 1.0f)
{
float radius = propagating_radius(cur);
Quad to_draw = quad_centered(cur->from, V2(radius, radius));
draw_quad((DrawParams){true, to_draw, IMG(image_hovering_circle), blendalpha(WHITE, 1.0f - cur->progress)});
}
}
}
PROFILE_SCOPE("dialog menu") // big dialog panel draw big dialog panel PROFILE_SCOPE("dialog menu") // big dialog panel draw big dialog panel
{ {
static float on_screen = 0.0f; static float on_screen = 0.0f;
@ -4607,35 +4762,28 @@ void frame(void)
if (talking_to && aabb_is_valid(dialog_panel)) if (talking_to && aabb_is_valid(dialog_panel))
{ {
MD_ArenaTemp scratch = MD_GetScratch(0, 0); MD_ArenaTemp scratch = MD_GetScratch(0, 0);
Dialog dialog = produce_dialog(talking_to, true); Dialog dialog = get_dialog_elems(talking_to, true);
{ {
for (int i = dialog.cur_index - 1; i >= 0; i--) for (int i = dialog.cur_index - 1; i >= 0; i--)
{ {
DialogElement *it = &dialog.data[i]; DialogElement *it = &dialog.data[i];
{ {
Color color; Color color;
if(it->was_eavesdropped) if (it->kind == DELEM_PLAYER)
{
color = WHITE;
}
else if (it->kind == DELEM_NPC)
{
color = colhex(0x34e05c);
}
else if (it->kind == DELEM_ACTION_DESCRIPTION)
{ {
color = colhex(0xcb40e6); color = colhex(0xebc334);
} }
else else
{ {
if (it->kind == DELEM_PLAYER) assert(false);
{
color = WHITE;
}
else if (it->kind == DELEM_NPC)
{
color = colhex(0x34e05c);
}
else if (it->kind == DELEM_ACTION_DESCRIPTION)
{
color = colhex(0xebc334);
}
else
{
assert(false);
}
} }
color = blendalpha(color, alpha); color = blendalpha(color, alpha);

@ -97,16 +97,20 @@ typedef struct Action
MD_u8 speech[MAX_SENTENCE_LENGTH]; MD_u8 speech[MAX_SENTENCE_LENGTH];
int speech_length; int speech_length;
bool talking_to_somebody;
NpcKind talking_to_kind;
MD_u8 internal_monologue[MAX_SENTENCE_LENGTH]; MD_u8 internal_monologue[MAX_SENTENCE_LENGTH];
int internal_monologue_length; int internal_monologue_length;
} Action; } Action;
typedef struct typedef struct
{ {
bool eavesdropped_from_party;
bool i_said_this; // don't trigger npc action on own self memory modification bool i_said_this; // don't trigger npc action on own self memory modification
NpcKind author_npc_kind; // only valid if author is AuthorNpc NpcKind author_npc_kind; // only valid if author is AuthorNpc
bool was_directed_at_somebody; bool was_talking_to_somebody;
NpcKind directed_at_kind; NpcKind talking_to_kind;
bool heard_physically; // if not physically, the source was directly
bool dont_show_to_player; // jester and past memories are hidden to the player when made into dialog bool dont_show_to_player; // jester and past memories are hidden to the player when made into dialog
} MemoryContext; } MemoryContext;
@ -275,6 +279,8 @@ typedef struct Entity
float anim_change_timer; float anim_change_timer;
} Entity; } Entity;
typedef BUFF(NpcKind, 32) CanTalkTo;
bool npc_is_knight_sprite(Entity *it) bool npc_is_knight_sprite(Entity *it)
{ {
return it->is_npc && (false return it->is_npc && (false
@ -360,7 +366,7 @@ typedef struct GameState {
Entity entities[MAX_ENTITIES]; Entity entities[MAX_ENTITIES];
} GameState; } GameState;
#define ENTITIES_ITER(ents) for (Entity *it = ents; it < ents + ARRLEN(ents); it++) if (it->exists) #define ENTITIES_ITER(ents) for (Entity *it = ents; it < ents + ARRLEN(ents); it++) if (it->exists && !it->destroy && it->generation > 0)
bool npc_does_dialog(Entity *it) bool npc_does_dialog(Entity *it)
{ {
@ -398,49 +404,9 @@ MD_String8List held_item_strings(MD_Arena *arena, Entity *e)
return to_return; return to_return;
} }
// returns reason why allocated on arena if invalid
// to might be null here, from can't be null
MD_String8 is_action_valid(MD_Arena *arena, Entity *from, Entity *to_might_be_null, Action a)
{
assert(a.speech_length <= MAX_SENTENCE_LENGTH && a.speech_length >= 0);
assert(a.kind >= 0 && a.kind < ARRLEN(actions));
assert(from);
if(a.kind == ACT_give_item)
{
assert(a.argument.item_to_give >= 0 && a.argument.item_to_give < ARRLEN(items));
bool has_it = false;
BUFF_ITER(ItemKind, &from->held_items)
{
if(*it == a.argument.item_to_give)
{
has_it = true;
break;
}
}
if(!has_it)
{
MD_StringJoin join = {.mid = MD_S8Lit(", ")};
return FmtWithLint(arena, "Can't give item `ITEM_%s`, you only have [%.*s] in your inventory", items[a.argument.item_to_give].enum_name, MD_S8VArg(MD_S8ListJoin(arena, held_item_strings(arena, from), &join)));
}
if(!to_might_be_null)
{
return MD_S8Lit("You can't give an item to nobody, you're currently not in conversation or targeting somebody.");
}
}
if(a.kind == ACT_leaves_player && from->standing != STANDING_JOINED)
{
return MD_S8Lit("You can't leave the player unless you joined them.");
}
return (MD_String8){0};
}
// outputs json // outputs json
MD_String8 generate_chatgpt_prompt(MD_Arena *arena, Entity *e) MD_String8 generate_chatgpt_prompt(MD_Arena *arena, Entity *e, CanTalkTo can_talk_to)
{ {
assert(e->is_npc); assert(e->is_npc);
assert(e->npc_kind < ARRLEN(characters)); assert(e->npc_kind < ARRLEN(characters));
@ -461,7 +427,7 @@ MD_String8 generate_chatgpt_prompt(MD_Arena *arena, Entity *e)
{ {
if(it->is_error) if(it->is_error)
{ {
MD_S8ListPush(scratch.arena, &list, make_json_node(scratch.arena, MSG_SYSTEM, FmtWithLint(scratch.arena, "ERROR, what you said is incorrect because: %.*s", it->speech_length, it->speech))); MD_S8ListPush(scratch.arena, &list, make_json_node(scratch.arena, MSG_USER, FmtWithLint(scratch.arena, "ERROR, what you said is incorrect because: %.*s", it->speech_length, it->speech)));
} }
else else
{ {
@ -472,10 +438,14 @@ MD_String8 generate_chatgpt_prompt(MD_Arena *arena, Entity *e)
PushWithLint(scratch.arena, &cur_list, "{"); PushWithLint(scratch.arena, &cur_list, "{");
if(!it->context.i_said_this) if(!it->context.i_said_this)
{ {
PushWithLint(scratch.arena, &cur_list, "character: %s, ", characters[it->context.author_npc_kind].name); PushWithLint(scratch.arena, &cur_list, "who_i_am: %s, ", characters[it->context.author_npc_kind].name);
} }
MD_String8 speech = MD_S8(it->speech, it->speech_length); MD_String8 speech = MD_S8(it->speech, it->speech_length);
PushWithLint(scratch.arena, &cur_list, "was_heard_in_passing: %s, ", it->context.heard_physically ? "true" : "false");
PushWithLint(scratch.arena, &cur_list, "talking_to: %s, ", it->context.was_talking_to_somebody ? characters[it->context.talking_to_kind].enum_name : "nobody");
// add speech // add speech
{ {
if(it->context.author_npc_kind == NPC_Player) if(it->context.author_npc_kind == NPC_Player)
@ -595,6 +565,13 @@ MD_String8 generate_chatgpt_prompt(MD_Arena *arena, Entity *e)
} }
PushWithLint(scratch.arena, &latest_state, "]\n"); PushWithLint(scratch.arena, &latest_state, "]\n");
PushWithLint(scratch.arena, &latest_state, "The characters close enough for you to talk to with `talking_to`: [");
BUFF_ITER(NpcKind, &can_talk_to)
{
PushWithLint(scratch.arena, &latest_state, "%s, ", characters[*it].enum_name);
}
PushWithLint(scratch.arena, &latest_state, "]\n");
// last thought explanation and re-prompt // last thought explanation and re-prompt
{ {
MD_String8 last_thought_string = {0}; MD_String8 last_thought_string = {0};
@ -650,18 +627,33 @@ MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 sentenc
MD_String8 speech_str = {0}; MD_String8 speech_str = {0};
MD_String8 thoughts_str = {0}; MD_String8 thoughts_str = {0};
MD_String8 action_arg_str = {0}; MD_String8 action_arg_str = {0};
MD_String8 who_i_am_str = {0};
MD_String8 talking_to_str = {0};
if(error_message.size == 0) if(error_message.size == 0)
{ {
action_str = get_field(message_obj, MD_S8Lit("action")); action_str = get_field(message_obj, MD_S8Lit("action"));
who_i_am_str = get_field(message_obj, MD_S8Lit("who_i_am"));
speech_str = get_field(message_obj, MD_S8Lit("speech")); speech_str = get_field(message_obj, MD_S8Lit("speech"));
thoughts_str = get_field(message_obj, MD_S8Lit("thoughts")); thoughts_str = get_field(message_obj, MD_S8Lit("thoughts"));
action_arg_str = get_field(message_obj, MD_S8Lit("action_arg")); action_arg_str = get_field(message_obj, MD_S8Lit("action_arg"));
talking_to_str = get_field(message_obj, MD_S8Lit("talking_to"));
}
if(error_message.size == 0 && who_i_am_str.size == 0)
{
error_message = MD_S8Lit("Expected field named `who_i_am` in message");
} }
if(error_message.size == 0 && action_str.size == 0) if(error_message.size == 0 && action_str.size == 0)
{ {
error_message = MD_S8Lit("Expected field named `action` in message"); error_message = MD_S8Lit("Expected field named `action` in message");
} }
if(error_message.size == 0 && talking_to_str.size == 0)
{
error_message = MD_S8Lit("Expected field named `talking_to` in message");
}
if(error_message.size == 0 && thoughts_str.size == 0)
{
error_message = MD_S8Lit("Expected field named `thoughts` in message, and to have nonzero size");
}
if(error_message.size == 0 && speech_str.size >= MAX_SENTENCE_LENGTH) if(error_message.size == 0 && speech_str.size >= MAX_SENTENCE_LENGTH)
{ {
error_message = FmtWithLint(arena, "Speech string provided is too big, maximum bytes is %d", MAX_SENTENCE_LENGTH); error_message = FmtWithLint(arena, "Speech string provided is too big, maximum bytes is %d", MAX_SENTENCE_LENGTH);
@ -671,6 +663,37 @@ MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 sentenc
error_message = FmtWithLint(arena, "Thoughts string provided is too big, maximum bytes is %d", MAX_SENTENCE_LENGTH); error_message = FmtWithLint(arena, "Thoughts string provided is too big, maximum bytes is %d", MAX_SENTENCE_LENGTH);
} }
assert(!e->is_character); // player can't perform AI actions?
MD_String8 my_name = MD_S8CString(characters[e->npc_kind].enum_name);
if(error_message.size == 0 && !MD_S8Match(who_i_am_str, my_name, 0))
{
error_message = FmtWithLint(arena, "You are acting as %.*s, not what you said in who_i_am, `%.*s`", MD_S8VArg(my_name), MD_S8VArg(who_i_am_str));
}
if(error_message.size == 0)
{
if(MD_S8Match(talking_to_str, MD_S8Lit("nobody"), 0))
{
out->talking_to_somebody = false;
}
else
{
bool found = false;
for(int i = 0; i < ARRLEN(characters); i++)
{
if(MD_S8Match(talking_to_str, MD_S8CString(characters[i].enum_name), 0))
{
found = true;
out->talking_to_kind = i;
}
}
if(!found)
{
error_message = FmtWithLint(arena, "Unrecognized character provided in talking_to: `%.*s`", MD_S8VArg(talking_to_str));
}
}
}
if(error_message.size == 0) if(error_message.size == 0)
{ {
memcpy(out->speech, speech_str.str, speech_str.size); memcpy(out->speech, speech_str.str, speech_str.size);

@ -1,11 +1,12 @@
DONE - rewrite to have metadesk format for speech and actions DONE - rewrite to have metadesk format for speech and actions
- action and item explanations in system message, along with available actions and items DONE - action and item explanations in system message, along with available actions and items
- remove party eavesdropping, but make clear to AI when things are heard physically or told directly. Allow AI to choose people in vicinity to target with conversation and action. I.e a `talking_to` field. Also add a required character: field in chatgpt response, and make sure it matches the character it's supposed to act as. DONE - remove party eavesdropping, but make clear to AI when things are heard physically or told directly. Allow AI to choose people in vicinity to target with conversation and action. I.e a `talking_to` field. Also add a required character: field in chatgpt response, and make sure it matches the character it's supposed to act as.
- The using of items and backpack inventory view.
- delete peace tokens, replace with key items and scroll item that says word. - delete peace tokens, replace with key items and scroll item that says word.
- display actions, like giving an item, in memory history - display actions, like giving an item, in memory history
- door npc that refuses to open unless player says the word - door npc that refuses to open unless player says the word
- The Mighty Sword that's conservative, when convinced to be pulled player gets it as an item - The Mighty Sword that's conservative, when convinced to be pulled player gets it as an item
- The using of items and backpack inventory view. - Change give_item to give_item_for_free, and add propose_trade that does trade workflow so NPCs can't easily be screwed over

@ -10,6 +10,7 @@
#define PERCEPTION_HEARING_RAGE (TILE_SIZE*4.0f) #define PERCEPTION_HEARING_RAGE (TILE_SIZE*4.0f)
#define CHARACTERS_PER_SEC 45.0f #define CHARACTERS_PER_SEC 45.0f
#define PEACE_TOKENS_NEEDED 5 #define PEACE_TOKENS_NEEDED 5
#define PROPAGATE_ACTIONS_RADIUS (TILE_SIZE*5.0f)
#define ARENA_SIZE (1024*1024) #define ARENA_SIZE (1024*1024)

Loading…
Cancel
Save