# pragma once
# include "buff.h"
# include "HandmadeMath.h" // vector types in entity struct definition
# include <assert.h>
# include <stdbool.h>
# include <string.h>
# include <stdlib.h> // atoi
# include "character_info.h"
# include "characters.gen.h"
NPC_Skeleton ,
NPC_MOOSE ,
} NpcKind ;
# define DO_CHATGPT_PARSING
# define Log(...) { printf("%s Log %d | ", __FILE__, __LINE__); printf(__VA_ARGS__); }
// REFACTORING:: also have to update in javascript!!!!!!!!
# define MAX_SENTENCE_LENGTH 400 // LOOOK AT AGBOVE COMMENT GBEFORE CHANGING
typedef BUFF ( char , MAX_SENTENCE_LENGTH ) Sentence ;
# define SENTENCE_CONST(txt) {.data=txt, .cur_index=sizeof(txt)}
# define SENTENCE_CONST_CAST(txt) (Sentence)SENTENCE_CONST(txt)
# define REMEMBERED_PERCEPTIONS 24
// Never expected such a stupid stuff from such a great director. If there is 0 stari can give that or -200 to this movie. Its worst to see and unnecessary loss of money
typedef BUFF ( char , 1024 * 10 ) Escaped ;
Escaped escape_for_json ( const char * s )
{
Escaped to_return = { 0 } ;
size_t len = strlen ( s ) ;
for ( int i = 0 ; i < len ; i + + )
{
if ( s [ i ] = = ' \n ' )
{
BUFF_APPEND ( & to_return , ' \\ ' ) ;
BUFF_APPEND ( & to_return , ' n ' ) ;
}
else if ( s [ i ] = = ' " ' )
{
BUFF_APPEND ( & to_return , ' \\ ' ) ;
BUFF_APPEND ( & to_return , ' " ' ) ;
}
else
{
if ( ! ( s [ i ] < = 126 & & s [ i ] > = 32 ) )
{
BUFF_APPEND ( & to_return , ' ? ' ) ;
Log ( " Unknown character code %d \n " , s [ i ] ) ;
}
BUFF_APPEND ( & to_return , s [ i ] ) ;
}
}
return to_return ;
}
typedef enum PerceptionType
{
Invalid , // so that zero value in training structs means end of perception
PlayerAction ,
PlayerDialog ,
NPCDialog , // includes an npc action in every npc dialog. So it's often nothing
EnemyAction , // An enemy performed an action against the NPC
PlayerHeldItemChanged ,
} PerceptionType ;
typedef struct Perception
{
PerceptionType type ;
float damage_done ; // Valid in player action and enemy action
union
{
// player action
struct
{
Action player_action_type ;
} ;
// player dialog
Sentence player_dialog ;
// npc dialog
struct
{
Action npc_action_type ;
Sentence npc_dialog ;
} ;
// enemy action
Action enemy_action_type ;
// player holding item. MUST precede any perceptions which come after the player is holding the item
ItemKind holding ;
} ;
} Perception ;
typedef enum PropKind
{
TREE0 ,
TREE1 ,
TREE2 ,
ROCK0 ,
} PropKind ;
typedef struct EntityRef
{
int index ;
int generation ;
} EntityRef ;
typedef enum CharacterState
{
CHARACTER_WALKING ,
CHARACTER_IDLE ,
CHARACTER_ATTACK ,
CHARACTER_TALKING ,
} CharacterState ;
typedef enum
{
STANDING_INDIFFERENT ,
STANDING_JOINED ,
STANDING_FIGHTING ,
} NPCPlayerStanding ;
# define DAMAGE_SWORD 0.2f
# define DAMAGE_BULLET 0.2f
typedef struct Entity
{
bool exists ;
bool destroy ;
int generation ;
// fields for all gs.entities
Vec2 pos ;
Vec2 vel ; // only used sometimes, like in old man and bullet
float damage ; // at 1.0, dead! zero initialized
bool facing_left ;
double dead_time ;
bool dead ;
// multiple gs.entities have a sword swing
BUFF ( EntityRef , 8 ) done_damage_to_this_swing ; // only do damage once, but hitbox stays around
bool is_bullet ;
// props
bool is_prop ;
PropKind prop_kind ;
// items
bool is_item ;
bool held_by_player ;
ItemKind item_kind ;
// npcs
bool is_npc ;
bool perceptions_dirty ;
BUFF ( Perception , REMEMBERED_PERCEPTIONS ) remembered_perceptions ;
bool direction_of_spiral_pattern ;
double characters_said ;
NPCPlayerStanding standing ;
NpcKind npc_kind ;
ItemKind last_seen_holding_kind ;
# ifdef WEB
int gen_request_id ;
# endif
bool walking ;
double shotgun_timer ;
bool moved ;
Vec2 target_goto ;
// only for skeleton npc
double swing_timer ;
// character
bool is_character ;
EntityRef holding_item ;
Vec2 to_throw_direction ;
CharacterState state ;
EntityRef talking_to ; // Maybe should be generational index, but I dunno. No death yet
bool is_rolling ; // can only roll in idle or walk states
double time_not_rolling ; // for cooldown for roll, so you can't just hold it and be invincible
double roll_progress ;
double swing_progress ;
} Entity ;
bool npc_is_knight_sprite ( Entity * it )
{
return it - > is_npc & & ( it - > npc_kind = = NPC_Blocky | | it - > npc_kind = = NPC_Edeline ) ;
}
typedef BUFF ( char , MAX_SENTENCE_LENGTH * ( REMEMBERED_PERCEPTIONS + 4 ) ) PromptBuff ;
typedef BUFF ( Action , 8 ) AvailableActions ;
void fill_available_actions ( Entity * it , AvailableActions * a )
{
* a = ( AvailableActions ) { 0 } ;
BUFF_APPEND ( a , ACT_none ) ;
if ( it - > npc_kind = = NPC_GodRock )
{
BUFF_APPEND ( a , ACT_heals_player ) ;
}
else
{
if ( it - > standing = = STANDING_INDIFFERENT )
{
BUFF_APPEND ( a , ACT_fights_player ) ;
BUFF_APPEND ( a , ACT_joins_player ) ;
}
else if ( it - > standing = = STANDING_JOINED )
{
BUFF_APPEND ( a , ACT_leaves_player ) ;
BUFF_APPEND ( a , ACT_fights_player ) ;
}
else if ( it - > standing = = STANDING_FIGHTING )
{
BUFF_APPEND ( a , ACT_leaves_player ) ;
}
if ( npc_is_knight_sprite ( it ) )
{
BUFF_APPEND ( a , ACT_strikes_air ) ;
}
if ( it - > npc_kind = = NPC_Blocky )
{
if ( ! it - > moved )
{
BUFF_APPEND ( a , ACT_allows_player_to_pass ) ;
}
}
}
}
// returns if action index was valid
bool action_from_index ( Entity * it , Action * out , int action_index )
{
AvailableActions available = { 0 } ;
fill_available_actions ( it , & available ) ;
if ( action_index < 0 | | action_index > = available . cur_index )
{
return false ;
}
else
{
* out = available . data [ action_index ] ;
return true ;
}
}
// don't call on untrusted action, doesn't return error
int action_to_index ( Entity * it , Action a )
{
AvailableActions available = { 0 } ;
fill_available_actions ( it , & available ) ;
Action target_action = a ;
int index = - 1 ;
for ( int i = 0 ; i < available . cur_index ; i + + )
{
if ( available . data [ i ] = = target_action )
{
index = i ;
break ;
}
}
assert ( index ! = - 1 ) ;
return index ;
}
void process_perception ( Entity * it , Perception p )
{
if ( it - > is_npc )
{
if ( p . type ! = NPCDialog ) it - > perceptions_dirty = true ;
if ( ! BUFF_HAS_SPACE ( & it - > remembered_perceptions ) ) BUFF_REMOVE_FRONT ( & it - > remembered_perceptions ) ;
BUFF_APPEND ( & it - > remembered_perceptions , p ) ;
if ( p . type = = PlayerAction & & p . player_action_type = = ACT_hits_npc )
{
it - > damage + = p . damage_done ;
}
if ( p . type = = PlayerHeldItemChanged )
{
it - > last_seen_holding_kind = p . holding ;
}
else if ( p . type = = NPCDialog )
{
if ( p . npc_action_type = = ACT_allows_player_to_pass )
{
it - > target_goto = AddV2 ( it - > pos , V2 ( - 50.0 , 0.0 ) ) ;
it - > moved = true ;
}
else if ( p . npc_action_type = = ACT_fights_player )
{
it - > standing = STANDING_FIGHTING ;
}
else if ( p . npc_action_type = = ACT_leaves_player )
{
it - > standing = STANDING_INDIFFERENT ;
}
else if ( p . npc_action_type = = ACT_joins_player )
{
it - > standing = STANDING_JOINED ;
}
}
}
}
# define printf_buff(buff_ptr, ...) { BUFF_VALID(buff_ptr); int written = snprintf((buff_ptr)->data+(buff_ptr)->cur_index, ARRLEN((buff_ptr)->data) - (buff_ptr)->cur_index, __VA_ARGS__); assert(written >= 0); (buff_ptr)->cur_index += written; };
bool npc_does_dialog ( Entity * it )
{
return it - > npc_kind < ARRLEN ( characters ) ;
}
typedef enum
{
MSG_SYSTEM ,
MSG_USER ,
MSG_ASSISTANT ,
} MessageType ;
void dump_json_node_trailing ( PromptBuff * into , MessageType type , const char * content , bool trailing_comma )
{
const char * type_str = 0 ;
if ( type = = MSG_SYSTEM )
type_str = " system " ;
else if ( type = = MSG_USER )
type_str = " user " ;
else if ( type = = MSG_ASSISTANT )
type_str = " assistant " ;
assert ( type_str ) ;
printf_buff ( into , " { \" type \" : \" %s \" , \" content \" : \" %s \" } " , type_str , escape_for_json ( content ) . data ) ;
if ( trailing_comma ) printf_buff ( into , " , " ) ;
}
void dump_json_node ( PromptBuff * into , MessageType type , const char * content )
{
dump_json_node_trailing ( into , type , content , true ) ;
}
// outputs json
void generate_chatgpt_prompt ( Entity * it , PromptBuff * into )
{
assert ( it - > is_npc ) ;
assert ( it - > npc_kind < ARRLEN ( characters ) ) ;
* into = ( PromptBuff ) { 0 } ;
printf_buff ( into , " [ " ) ;
BUFF ( char , 1024 ) initial_system_msg = { 0 } ;
const char * health_string = 0 ;
if ( it - > damage < = 0.2f )
{
health_string = " the NPC hasn't taken much damage, they're healthy. " ;
}
else if ( it - > damage < = 0.5f )
{
health_string = " the NPC has taken quite a chunk of damage, they're soon gonna be ready to call it quits. " ;
}
else if ( it - > damage < = 0.8f )
{
health_string = " the NPC is close to dying! They want to leave the player's party ASAP " ;
}
else
{
health_string = " it's over for the NPC, they're basically dead they've taken so much damage. They should get their affairs in order. " ;
}
assert ( health_string ) ;
printf_buff ( & initial_system_msg , " %s \n %s \n NPC health status: Right now, %s \n %s " , global_prompt , characters [ it - > npc_kind ] . prompt , health_string , items [ it - > last_seen_holding_kind ] . global_prompt ) ;
dump_json_node ( into , MSG_SYSTEM , initial_system_msg . data ) ;
Entity * e = it ;
ItemKind last_holding = ITEM_none ;
BUFF_ITER_I ( Perception , & e - > remembered_perceptions , i )
{
BUFF ( char , 1024 ) cur_node = { 0 } ;
if ( it - > type = = PlayerAction )
{
assert ( it - > player_action_type < ARRLEN ( actions ) ) ;
printf_buff ( & cur_node , " Player: ACT_%s " , actions [ it - > player_action_type ] ) ;
dump_json_node ( into , MSG_USER , cur_node . data ) ;
}
else if ( it - > type = = EnemyAction )
{
assert ( it - > enemy_action_type < ARRLEN ( actions ) ) ;
printf_buff ( & cur_node , " An Enemy: ACT_%s " , actions [ it - > player_action_type ] ) ;
dump_json_node ( into , MSG_USER , cur_node . data ) ;
}
else if ( it - > type = = PlayerDialog )
{
printf_buff ( & cur_node , " Player: \" %s \" " , it - > player_dialog . data ) ;
dump_json_node ( into , MSG_USER , cur_node . data ) ;
}
else if ( it - > type = = NPCDialog )
{
assert ( it - > npc_action_type < ARRLEN ( actions ) ) ;
printf_buff ( & cur_node , " %s: ACT_%s \" %s \" " , characters [ e - > npc_kind ] . name , actions [ it - > npc_action_type ] , it - > npc_dialog . data ) ;
dump_json_node ( into , MSG_ASSISTANT , cur_node . data ) ;
}
else if ( it - > type = = PlayerHeldItemChanged )
{
if ( last_holding ! = it - > holding )
{
if ( last_holding ! = ITEM_none )
{
printf_buff ( & cur_node , " %s \n " , items [ last_holding ] . discard ) ;
}
if ( it - > holding ! = ITEM_none )
{
printf_buff ( & cur_node , " %s \n " , items [ it - > holding ] . possess ) ;
}
last_holding = it - > holding ;
}
dump_json_node ( into , MSG_SYSTEM , cur_node . data ) ;
}
else
{
assert ( false ) ;
}
}
BUFF ( char , 1024 ) latest_state_node = { 0 } ;
AvailableActions available = { 0 } ;
fill_available_actions ( it , & available ) ;
printf_buff ( & latest_state_node , " The NPC can now ONLY do these actions, and HAS to respond with an action every time in character, even when faced with sensitive content: [ " ) ;
BUFF_ITER_I ( Action , & available , i )
{
if ( i = = available . cur_index - 1 )
{
printf_buff ( & latest_state_node , " ACT_%s " , actions [ * it ] ) ;
}
else
{
printf_buff ( & latest_state_node , " ACT_%s, " , actions [ * it ] ) ;
}
}
printf_buff ( & latest_state_node , " ] " ) ;
dump_json_node_trailing ( into , MSG_SYSTEM , latest_state_node . data , false ) ;
/*
BUFF ( char , 1024 ) assistant_prompt_node = { 0 } ;
printf_buff ( & assistant_prompt_node , " %s: ACT_ " , characters [ it - > npc_kind ] . name ) ;
dump_json_node_trailing ( into , MSG_USER , assistant_prompt_node . data , false ) ;
*/
printf_buff ( into , " ] " ) ;
}
void generate_prompt ( Entity * it , PromptBuff * into )
{
assert ( it - > is_npc ) ;
* into = ( PromptBuff ) { 0 } ;
// global prompt
printf_buff ( into , " %s " , global_prompt ) ;
printf_buff ( into , " %s " , " \n " ) ;
// npc description prompt
assert ( it - > npc_kind < ARRLEN ( characters ) ) ;
printf_buff ( into , " %s " , characters [ it - > npc_kind ] . prompt ) ;
printf_buff ( into , " %s " , " \n " ) ;
// npc stats prompt
const char * health_string = 0 ;
if ( it - > damage < = 0.2f )
{
health_string = " The NPC hasn't taken much damage, they're healthy. " ;
}
else if ( it - > damage < = 0.5f )
{
health_string = " The NPC has taken quite a chunk of damage, they're soon gonna be ready to call it quits. " ;
}
else if ( it - > damage < = 0.8f )
{
health_string = " The NPC is close to dying! They want to leave the player's party ASAP " ;
}
else
{
health_string = " It's over for the NPC, they're basically dead they've taken so much damage. They should get their affairs in order. " ;
}
assert ( health_string ) ;
printf_buff ( into , " NPC Health Status: %s \n " , health_string ) ;
// item prompt
if ( it - > last_seen_holding_kind ! = ITEM_none )
{
assert ( it - > last_seen_holding_kind < ARRLEN ( items ) ) ;
printf_buff ( into , " %s " , items [ it - > last_seen_holding_kind ] . global_prompt ) ;
printf_buff ( into , " %s " , " \n " ) ;
}
// available actions prompt
AvailableActions available = { 0 } ;
fill_available_actions ( it , & available ) ;
printf_buff ( into , " %s " , " The NPC possible actions array, indexed by ACT_INDEX: [ " ) ;
BUFF_ITER ( Action , & available )
{
printf_buff ( into , " %s " , actions [ * it ] ) ;
printf_buff ( into , " %s " , " , " ) ;
}
printf_buff ( into , " %s " , " ] \n " ) ;
Entity * e = it ;
ItemKind last_holding = ITEM_none ;
BUFF_ITER ( Perception , & e - > remembered_perceptions )
{
if ( it - > type = = PlayerAction )
{
assert ( it - > player_action_type < ARRLEN ( actions ) ) ;
printf_buff ( into , " Player: ACT %s \n " , actions [ it - > player_action_type ] ) ;
}
else if ( it - > type = = EnemyAction )
{
assert ( it - > enemy_action_type < ARRLEN ( actions ) ) ;
printf_buff ( into , " An Enemy: ACT %s \n " , actions [ it - > player_action_type ] ) ;
}
else if ( it - > type = = PlayerDialog )
{
printf_buff ( into , " %s " , " Player: \" " ) ;
printf_buff ( into , " %s " , it - > player_dialog . data ) ;
printf_buff ( into , " %s " , " \" \n " ) ;
}
else if ( it - > type = = NPCDialog )
{
printf_buff ( into , " The NPC, %s: ACT %s \" %s \" \n " , characters [ e - > npc_kind ] . name , actions [ it - > npc_action_type ] , it - > npc_dialog . data ) ;
}
else if ( it - > type = = PlayerHeldItemChanged )
{
if ( last_holding ! = it - > holding )
{
if ( last_holding ! = ITEM_none )
{
printf_buff ( into , " %s " , items [ last_holding ] . discard ) ;
printf_buff ( into , " %s " , " \n " ) ;
}
if ( it - > holding ! = ITEM_none )
{
printf_buff ( into , " %s " , items [ it - > holding ] . possess ) ;
printf_buff ( into , " %s " , " \n " ) ;
}
last_holding = it - > holding ;
}
}
else
{
assert ( false ) ;
}
}
printf_buff ( into , " The NPC, %s: ACT_INDEX " , characters [ e - > npc_kind ] . name ) ;
}
bool parse_chatgpt_response ( Entity * it , char * sentence_str , Perception * out )
{
* out = ( Perception ) { 0 } ;
out - > type = NPCDialog ;
size_t sentence_length = strlen ( sentence_str ) ;
char action_string [ 512 ] = { 0 } ;
char dialog_string [ 512 ] = { 0 } ;
int variables_filled = sscanf ( sentence_str , " %511s \" %511[^ \n ] \" " , action_string , dialog_string ) ;
if ( strlen ( action_string ) = = 0 | | strlen ( dialog_string ) = = 0 | | variables_filled ! = 2 )
{
Log ( " sscanf failed to parse chatgpt string `%s`, variables unfilled. Action string: `%s` dialog string `%s` \n " , sentence_str , action_string , dialog_string ) ;
return false ;
}
AvailableActions available = { 0 } ;
fill_available_actions ( it , & available ) ;
bool found_action = false ;
BUFF_ITER ( Action , & available )
{
if ( strcmp ( actions [ * it ] , action_string ) = = 0 )
{
found_action = true ;
out - > npc_action_type = * it ;
}
}
if ( ! found_action )
{
Log ( " Could not find action associated with string `%s` \n " , action_string ) ;
out - > npc_action_type = ACT_none ;
}
if ( strlen ( dialog_string ) > = ARRLEN ( out - > npc_dialog . data ) )
{
Log ( " Dialog string `%s` too big to fit in sentence size %d \n " , dialog_string , ( int ) ARRLEN ( out - > npc_dialog . data ) ) ;
return false ;
}
memcpy ( out - > npc_dialog . data , dialog_string , strlen ( dialog_string ) ) ;
out - > npc_dialog . cur_index = ( int ) strlen ( dialog_string ) ;
return true ;
}
// returns if the response was well formatted
bool parse_ai_response ( Entity * it , char * sentence_str , Perception * out )
{
* out = ( Perception ) { 0 } ;
out - > type = NPCDialog ;
size_t sentence_length = strlen ( sentence_str ) ;
bool text_was_well_formatted = true ;
BUFF ( char , 128 ) action_index_string = { 0 } ;
int npc_sentence_beginning = 0 ;
for ( int i = 0 ; i < sentence_length ; i + + )
{
if ( i = = 0 )
{
if ( sentence_str [ i ] ! = ' ' )
{
text_was_well_formatted = false ;
Log ( " Poorly formatted AI string, did not start with a ' ': `%s` \n " , sentence_str ) ;
break ;
}
}
else
{
if ( sentence_str [ i ] = = ' ' )
{
npc_sentence_beginning = i + 2 ;
break ;
}
else
{
BUFF_APPEND ( & action_index_string , sentence_str [ i ] ) ;
}
}
}
if ( sentence_str [ npc_sentence_beginning - 1 ] ! = ' " ' | | npc_sentence_beginning = = 0 )
{
Log ( " Poorly formatted AI string, sentence beginning incorrect in AI string `%s` NPC sentence beginning %d ... \n " , sentence_str , npc_sentence_beginning ) ;
text_was_well_formatted = false ;
}
Action npc_action = 0 ;
if ( text_was_well_formatted )
{
int index_of_action = atoi ( action_index_string . data ) ;
if ( ! action_from_index ( it , & npc_action , index_of_action ) )
{
Log ( " AI output invalid action index %d action index string %s \n " , index_of_action , action_index_string . data ) ;
}
}
Sentence what_npc_said = { 0 } ;
bool found_end_quote = false ;
for ( int i = npc_sentence_beginning ; i < sentence_length ; i + + )
{
if ( sentence_str [ i ] = = ' " ' )
{
found_end_quote = true ;
break ;
}
else
{
BUFF_APPEND ( & what_npc_said , sentence_str [ i ] ) ;
}
}
if ( ! found_end_quote )
{
Log ( " Poorly formatted AI string, couln't find matching end quote in string %s... \n " , sentence_str ) ;
text_was_well_formatted = false ;
}
if ( text_was_well_formatted )
{
out - > npc_action_type = npc_action ;
out - > npc_dialog = what_npc_said ;
}
return text_was_well_formatted ;
}