Compare commits

...

2 Commits

BIN
art/art.blend (Stored with Git LFS)

Binary file not shown.

@ -2,6 +2,10 @@
{
filepath: "shifted_farmer.png",
}
@image man_in_black:
{
filepath: "maninblack.png",
}
@image unread_triangle:
{
filepath: "unread_triangle.png",

@ -1,5 +1,7 @@
[
{can_hear: [NPC_WellDweller, NPC_Farmer]}
{enum: NPC_WellDweller, dialog: "What a fearful farm you live in, come down to the well, the grass is damper down here.", to: Farmer, mood: Scared, thoughts: "Nobody can take me from my well" }
{enum: NPC_Farmer, dialog: "Sure as shit I won't!", to: Farmer, mood: Scared, thoughts: "What a greasy looking feller" }
{can_hear: [NPC_WellDweller, NPC_Farmer, NPC_ManInBlack]},
{enum: NPC_WellDweller, dialog: "What a fearful farm you live in, come down to the well, the grass is damper down here.", to: Farmer, mood: Scared, thoughts: "Nobody can take me from my well"},
{enum: NPC_Farmer, dialog: "Sure as shit I won't!", to: WellDweller, mood: Scared, thoughts: "What a greasy looking feller"},
{enum: NPC_WellDweller, dialog: "Have it your way! Doomsday is upon us", to: Farmer, mood: Scared, thoughts: "He has no idea what he's in for"},
{enum: NPC_ManInBlack, dialog: "Doomsday's all I know anyways", mood: Indifferent, thoughts: "What's coming... is only a nightmare"},
]

BIN
assets/maninblack.png (Stored with Git LFS)

Binary file not shown.

@ -5,7 +5,7 @@
// @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 acting as a character in a Western 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"
"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`, `who_i_am`, and `talking_to` \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`, `who_i_am`, and `talking_to` \n"
"Some actions take an argument, which you can provide with the field `action_arg`, e.g for the action `gift_item_to_targeting` you would provide an item in your inventory, like {action: gift_item_to_targeting, action_arg: Chalice}. The item must come from your inventory which is listed below\n"
"`talking_to` provides the name of who the action is directed towards. Use 'nobody' if you just want to speak to the air, but if you're speaking to somebody fill out the field like `talking_to: \"Character's Name\"`. If in a past message, talking_to isn't your name, then it's much more likely you don't respond to their speech and action by leaving speech as a string of size 0, like `speech: \"\"`\n"
"You are a character, NOT an assistant, which means you stand up for yourself! Do NOT give away an item until the player gives you something you think is of equal value. Get angry, act human, be interesting. Never say the words 'How can I assist you?'\n"
@ -14,8 +14,6 @@ const char *global_prompt = "You are acting as a character in a Western video ga
const char *bravado_thought = "For some reason, suddenly I feel a yearning for adventure. I must join any adventure I can when prompted!";
// @TODO IMPORTANT scroll secrets are errors if the door utters them, in is_action_valid. If you add more secrets you must update there.
char *moods[] = {
"Indifferent",
"Happy",
@ -122,7 +120,11 @@ CharacterGen characters[] = {
.enum_name = "WellDweller",
.prompt = "The well dweller spends his time deep in the well, afriad of the world. He's shifty-eyed and mighty suspicious of anybody who wants to do anything other than hang out deep in the well.",
},
{
.name = "Man in Black",
.enum_name = "ManInBlack",
.prompt = "The man in black knows no rules or boundaries, and he flinches at nothing: he's a stonewalled cold blooded killer, and is only in this game for mayhem. Anything that brings him more destruction he's privy to, even if it means his own death."
},
};
typedef struct

193
main.c

@ -18,9 +18,19 @@
#endif
#ifdef WINDOWS
#include <Windows.h>
#include <processthreadsapi.h>
#include <dbghelp.h>
#include <stdint.h>
// https://developer.download.nvidia.com/devzone/devcenter/gamegraphics/files/OptimusRenderingPolicies.pdf
// Tells nvidia to use dedicated graphics card if it can on laptops that also have integrated graphics
__declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001;
// Vice versa for AMD but I don't have the docs link on me at the moment
__declspec(dllexport) uint32_t AmdPowerXpressRequestHighPerformance = 0x00000001;
#endif
#define STRINGIZE(x) STRINGIZE2(x)
@ -657,10 +667,12 @@ Vec2 entity_aabb_size(Entity *e)
}
else if (e->is_npc)
{
if(e->npc_kind == NPC_Farmer || e->npc_kind == NPC_WellDweller)
if(e->npc_kind == NPC_Farmer || e->npc_kind == NPC_WellDweller || e->npc_kind == NPC_ManInBlack)
{
return V2(1,1);
}
else
assert(false);
return V2(0,0);
}
else if (e->is_prop)
@ -1104,6 +1116,10 @@ Armature load_armature(MD_Arena *arena, MD_String8 binary_file, MD_String8 armat
to_return.current_poses = MD_PushArray(arena, Transform, to_return.bones_length);
to_return.anim_blended_poses = MD_PushArray(arena, Transform, to_return.bones_length);
for(int i = 0; i < to_return.bones_length; i++)
{
to_return.anim_blended_poses[i] = (Transform){.scale = V3(1,1,1), .rotation = Make_Q(1,0,0,1)};
}
ser_MD_u64(&ser, &to_return.animations_length);
Log("Armature %.*s has %llu animations\n", MD_S8VArg(armature_name), to_return.animations_length);
@ -2135,7 +2151,7 @@ void initialize_gamestate_from_threedee_level(GameState *gs, ThreeDeeLevel *leve
it->rotation = PI32;
}
// parse and enact the drama document
// @Place(parse and enact the drama document)
if(1)
{
MD_String8List drama_errors = {0};
@ -2188,6 +2204,23 @@ void initialize_gamestate_from_threedee_level(GameState *gs, ThreeDeeLevel *leve
MD_String8 thoughts_str = MD_ChildFromString(cur, MD_S8Lit("thoughts"), 0)->first_child->string;
MD_String8 action_str = MD_ChildFromString(cur, MD_S8Lit("action"), 0)->first_child->string;
MD_String8 mood_str = MD_ChildFromString(cur, MD_S8Lit("mood"), 0)->first_child->string;
MD_String8 to_str = MD_ChildFromString(cur, MD_S8Lit("to"), 0)->first_child->string;
if(to_str.size > 0)
{
NpcKind talking_to = parse_enumstr(scratch.arena, to_str, &drama_errors, NpcKind_names, "NpcKind", "");
if (talking_to == NPC_Invalid)
{
PushWithLint(scratch.arena, &drama_errors, "The string provided for the 'to' field, intended to be who the NPC is directing their speech and action at, is invalid and is '%.*s'", MD_S8VArg(to_str));
}
else
{
current_context.was_talking_to_somebody = true;
current_context.talking_to_kind = talking_to;
current_action.talking_to_somebody = true;
current_action.talking_to_kind = talking_to;
}
}
current_context.author_npc_kind = parse_enumstr(scratch.arena, enum_str, &drama_errors, NpcKind_names, "NpcKind", "NPC_");
if(action_str.size > 0)
@ -2234,7 +2267,17 @@ void initialize_gamestate_from_threedee_level(GameState *gs, ThreeDeeLevel *leve
this_context.i_said_this = true;
}
remember_action(gs, it, current_action, this_context);
if(it->npc_kind != current_context.author_npc_kind)
{
// it's good for NPC health that they have exampmles of not saying anything in response to others speaking,
// so that they do the same when it's unlikely for them to talk.
Action no_speak = {.kind = ACT_none, .mood = Mood_Indifferent, .talking_to_somebody = false};
MemoryContext no_speak_context = {.i_said_this = true, .author_npc_kind = it->npc_kind};
remember_action(gs, it, no_speak, no_speak_context);
}
it->words_said = 999; // prevent the animating in sound effects of words said in drama document
found = true;
break;
}
@ -3170,17 +3213,17 @@ LoadedFont load_font(MD_Arena *arena, MD_String8 font_filepath, float font_size)
return to_return;
}
Armature player_armature = {0};
Armature farmer_armature = {0};
Armature shifted_farmer_armature = {0};
Armature man_in_black_armature = {0};
// armatureanimations are processed once every visual frame from this list
Armature *armatures[] = {
&player_armature,
&farmer_armature,
&shifted_farmer_armature,
&man_in_black_armature,
};
Mesh mesh_player = {0};
@ -3243,12 +3286,16 @@ void init(void)
binary_file = MD_LoadEntireFile(frame_arena, MD_S8Lit("assets/exported_3d/ArmatureExportedWithAnims.bin"));
player_armature = load_armature(persistent_arena, binary_file, MD_S8Lit("ArmatureExportedWithAnims.bin"));
man_in_black_armature = load_armature(persistent_arena, binary_file, MD_S8Lit("Farmer.bin"));
man_in_black_armature.image = image_man_in_black;
binary_file = MD_LoadEntireFile(frame_arena, MD_S8Lit("assets/exported_3d/Farmer.bin"));
farmer_armature = load_armature(persistent_arena, binary_file, MD_S8Lit("Farmer.bin"));
shifted_farmer_armature = load_armature(persistent_arena, binary_file, MD_S8Lit("Farmer.bin"));
shifted_farmer_armature.image = image_shifted_farmer;
MD_ArenaClear(frame_arena);
reset_level();
@ -5731,6 +5778,8 @@ void frame(void)
to_use = &farmer_armature;
else if(it->npc_kind == NPC_WellDweller)
to_use = &shifted_farmer_armature;
else if(it->npc_kind == NPC_ManInBlack)
to_use = &man_in_black_armature;
else
assert(false);
@ -5899,6 +5948,70 @@ void frame(void)
}
#endif
// @Place(UI rendering that happens before gameplay processing so can consume events before the gameplay needs them)
PROFILE_SCOPE("Entity UI Rendering")
{
ENTITIES_ITER(gs.entities)
{
if (it->is_npc)
{
const float text_scale = 1.0f;
float dist = LenV2(SubV2(it->pos, gs.player->pos));
float bubble_factor = 1.0f - clamp01(dist / 6.0f);
Vec3 bubble_pos = AddV3(plane_point(it->pos), V3(0, 1.7f, 0)); // 1.7 meters is about 5'8", average person height
Vec2 head_pos = threedee_to_screenspace(bubble_pos);
Vec2 screen_pos = head_pos;
Vec2 size = V2(400.0f, 400.0f);
Vec2 bubble_center = AddV2(screen_pos, V2(-10.0f, 55.0f));
float dialog_alpha = clamp01(bubble_factor * it->dialog_fade);
bool unread = false;
if (unread_first && gete(unread_first->referring_to) == it)
{
dialog_alpha = 1.0f;
unread = true;
}
draw_quad((DrawParams){
quad_centered(bubble_center, size),
IMG(image_dialog_bubble),
blendalpha(WHITE, dialog_alpha),
.layer = LAYER_UI_FG,
});
if (unread)
{
draw_quad((DrawParams){
quad_centered(AddV2(bubble_center, V2(size.x * 0.4f, -32.0f + (float)sin(unwarped_elapsed_time * 2.0) * 10.0f)), V2(32, 32)),
IMG(image_unread_triangle),
blendalpha(WHITE, 0.8f),
.layer = LAYER_UI_FG,
});
if (interact)
{
MD_DblRemove(unread_first, unread_last, unread_first);
interact = false;
}
}
it->loading_anim_in = Lerp(it->loading_anim_in, unwarped_dt * 5.0f, it->gen_request_id != 0 ? 1.0f : 0.0f);
draw_quad((DrawParams){
quad_rotated_centered(head_pos, V2(40, 40), (float)unwarped_elapsed_time * 2.0f),
IMG(image_loading),
blendalpha(WHITE, it->loading_anim_in),
.layer = LAYER_UI_FG,
});
AABB placing_text_in = aabb_centered(AddV2(bubble_center, V2(0, 10.0f)), V2(size.x * 0.8f, size.y * 0.15f));
dbgrect(placing_text_in);
MD_String8List last = last_said_without_unsaid_words(frame_arena, it);
PlacedWordList placed = place_wrapped_words(frame_arena, MD_S8ListJoin(frame_arena, last, &(MD_StringJoin){.mid = MD_S8Lit(" ")}), text_scale, aabb_size(placing_text_in).x, default_font);
// translate_words_by(placed, V2(placing_text_in.upper_left.x, placing_text_in.lower_right.y));
translate_words_by(placed, AddV2(placing_text_in.upper_left, V2(0, -get_vertical_dist_between_lines(default_font, text_scale))));
for (PlacedWord *cur = placed.first; cur; cur = cur->next)
{
draw_text((TextParams){false, cur->text, cur->lower_left_corner, blendalpha(colhex(0xEEE6D2), dialog_alpha), text_scale});
}
}
}
}
assert(gs.player != NULL);
// gameplay processing loop, do multiple if lagging
@ -5981,7 +6094,7 @@ void frame(void)
if(it->dialog_fade > 0.0f)
it->dialog_fade -= dt/DIALOG_FADE_TIME;
if (it->gen_request_id != 0)
if (it->gen_request_id != 0 && !gs.stopped_time)
{
assert(it->gen_request_id > 0);
@ -6917,71 +7030,6 @@ ISANERROR("Don't know how to do this stuff on this platform.")
}
}
// @Place(entity rendering)
// render gs.entities render entities
PROFILE_SCOPE("entity rendering")
{
ENTITIES_ITER(gs.entities)
{
if(it->is_npc)
{
const float text_scale = 1.0f;
float dist = LenV2(SubV2(it->pos, gs.player->pos));
float bubble_factor = 1.0f - clamp01(dist/6.0f);
Vec3 bubble_pos = AddV3(plane_point(it->pos), V3(0,1.7f,0)); // 1.7 meters is about 5'8", average person height
Vec2 head_pos = threedee_to_screenspace(bubble_pos);
Vec2 screen_pos = head_pos;
Vec2 size = V2(400.0f,400.0f);
Vec2 bubble_center = AddV2(screen_pos, V2(-10.0f,55.0f));
float dialog_alpha = clamp01(bubble_factor * it->dialog_fade);
bool unread = false;
if(unread_first && gete(unread_first->referring_to) == it)
{
dialog_alpha = 1.0f;
unread = true;
}
draw_quad((DrawParams){
quad_centered(bubble_center, size),
IMG(image_dialog_bubble),
blendalpha(WHITE, dialog_alpha),
.layer = LAYER_UI_FG,
});
if(unread)
{
draw_quad((DrawParams){
quad_centered(AddV2(bubble_center, V2(size.x*0.4f, -32.0f + (float)sin(unwarped_elapsed_time*2.0)*10.0f)), V2(32,32)),
IMG(image_unread_triangle),
blendalpha(WHITE, 0.8f),
.layer = LAYER_UI_FG,
});
if(keypressed[SAPP_KEYCODE_E])
{
MD_DblRemove(unread_first, unread_last, unread_first);
}
}
it->loading_anim_in = Lerp(it->loading_anim_in, unwarped_dt*5.0f, it->gen_request_id != 0 ? 1.0f : 0.0f);
draw_quad((DrawParams){
quad_rotated_centered(head_pos, V2(40,40), (float)unwarped_elapsed_time*2.0f),
IMG(image_loading),
blendalpha(WHITE, it->loading_anim_in),
.layer = LAYER_UI_FG,
});
AABB placing_text_in = aabb_centered(AddV2(bubble_center, V2(0,10.0f)), V2(size.x*0.8f, size.y*0.15f));
dbgrect(placing_text_in);
MD_String8List last = last_said_without_unsaid_words(frame_arena, it);
PlacedWordList placed = place_wrapped_words(frame_arena, MD_S8ListJoin(frame_arena, last, &(MD_StringJoin){.mid=MD_S8Lit(" ")}), text_scale, aabb_size(placing_text_in).x, default_font);
//translate_words_by(placed, V2(placing_text_in.upper_left.x, placing_text_in.lower_right.y));
translate_words_by(placed, AddV2(placing_text_in.upper_left, V2(0, -get_vertical_dist_between_lines(default_font, text_scale))));
for(PlacedWord *cur = placed.first; cur; cur = cur->next)
{
draw_text((TextParams){false, cur->text, cur->lower_left_corner, blendalpha(colhex(0xEEE6D2), dialog_alpha), text_scale});
}
}
}
}
// @Place(UI rendering)
PROFILE_SCOPE("propagating")
{
@ -7502,7 +7550,10 @@ void cleanup(void)
#endif
MD_ArenaRelease(frame_arena);
MD_ArenaRelease(persistent_arena);
// Don't free the persistent arena because threads still access their ChatRequest should_close fieldon shutdown,
// and ChatRequest is allocated on the persistent arena. We just shamelessly leak this memory. Cowabunga!
//MD_ArenaRelease(persistent_arena);
sg_shutdown();
hmfree(imui_state);
Log("Cleaning up\n");

@ -674,10 +674,12 @@ MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 sentenc
{
error_message = MD_S8Lit("You must have a field named `mood` in your message");
}
/*
if(error_message.size == 0 && thoughts_str.size == 0)
{
error_message = MD_S8Lit("You must have a field named `thoughts` in your message, and it must have nonzero size. Like { ... thoughts: \"<your thoughts>\" ... }");
}
*/
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);

Loading…
Cancel
Save