Compare commits

...

2 Commits

@ -7,7 +7,7 @@
{enum: Raphael, dialog: "Yeah man, what's up with you?", to: Devil}
{enum: Devil, dialog: "Nunya!", to: Raphael}
{enum: Raphael, dialog: "What does 'Nunya' mean? A wild critter you are...", to: Devil}
{enum: Daniel, dialog: "YOU ASKED FOR IT! TAKE THIS!", action: ACT_fire_shotgun, to: Devil, action_argument: "The Devil"}
{enum: Daniel, dialog: "YOU ASKED FOR IT! TAKE THIS!", action: ACT_fire_shotgun, to: Devil}
{enum: Devil, dialog: "Cute! You think your little toys can harm me!", to: Daniel}
{enum: Daniel, dialog: "What the hell??", to: Devil}
{enum: Raphael, dialog: "I don't think we can harm him like that", to: Daniel}
@ -44,6 +44,12 @@
{enum: Devil, dialog: "You will fall!", to: Angel}
{enum: Angel, dialog: "Perhaps. And then I will rise as He has, from the corpse and ashes, the sun rises.", to: Devil}
{can_hear: [Passerby, Angel]}
{enum: Passerby, dialog: "fjdsklajf", to: Angel}
{enum: Angel, dialog: "Cryptic gibberish upon me, is casting a stone upon God", to: Passerby}
{enum: Passerby, dialog: "What is my purpose here?", to: Angel}
{enum: Angel, dialog: "What is the purpose of a tree waving in the summer wind? A river carving a path into the countryside? Only God knows his plan. But your purpose, here, has been assigned. You must kill raphael. Good luck.", to: Passerby, action: ACT_assign_gameplay_objective, action_argument: "KILL Raphael"}
/*
{can_hear: [WellDweller, Farmer, ManInBlack]},
{enum: 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"},

@ -38,6 +38,11 @@ ActionInfo actions[] = {
.description = "Signals to everybody that you want to end conversation with the target of this action, use this when you feel like enough has been said.",
ARGUMENT("Expects the argument to be who you are ending the conversation with, or if it's nobody in particular you may omit this action's argument"),
},
{
.name = "assign_gameplay_objective",
.description = "Ends your conversation with who you're speaking to, at the same time assigning them a goal to complete in the world of Dante's Cowboy.",
ARGUMENT("Expects the argument to be the gameplay objective assigned for them, it fits a specific syntax and is case sensitive: it must be `VERB SUBJECT`, you're given a list of accepted verbs and subjects earlier."),
},
{
.name = "join",
.description = "Joins somebody else's party, so you follow them everywhere",
@ -65,6 +70,10 @@ ActionInfo actions[] = {
},
};
char *verbs[] = {
"KILL",
};
typedef enum
{
MSG_SYSTEM,
@ -116,7 +125,7 @@ CharacterGen characters[] = {
{
.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.",
.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.",
.silence_factor = 0.0,
},
};

@ -149,6 +149,7 @@ int main(int argc, char **argv)
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);
GEN_ENUM(char*, verbs, "ObjectiveVerb", "ObjectiveVerb_names", *it, "%s\n");
fclose(char_header);

421
main.c

@ -1852,6 +1852,11 @@ 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;
}
MD_ReleaseScratch(scratch);
}
@ -2261,7 +2266,12 @@ void initialize_gamestate_from_threedee_level(GameState *gs, ThreeDeeLevel *leve
}
if(action_argument_str.size > 0)
{
current_action.argument.targeting = parse_enumstr(scratch.arena, action_argument_str, &drama_errors, NpcKind_names, "NpcKind", "");
MD_String8 error = {0};
parse_action_argument(scratch.arena, &error, current_action.kind, action_argument_str, &current_action.argument);
if(error.size > 0)
{
PushWithLint(scratch.arena, &drama_errors, "Error parsing argument: '%.*s'", MD_S8VArg(error));
}
}
if(dialog.size >= ARRLEN(current_action.speech.text))
@ -2686,7 +2696,16 @@ void end_text_input(char *what_player_said_cstr)
chunk_from_s8(&to_perform.speech, what_player_said);
if(gete(gs.player->talking_to))
if(!gs.no_angel_screen)
{
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);
}
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;
@ -2694,6 +2713,7 @@ void end_text_input(char *what_player_said_cstr)
perform_action(&gs, gs.player, to_perform);
}
}
MD_ReleaseScratch(scratch);
}
/*
@ -3911,9 +3931,11 @@ typedef enum
LAYER_WORLD,
LAYER_UI,
LAYER_UI_FG,
LAYER_ANGEL_SCREEN,
LAYER_UI_TEXTINPUT,
LAYER_SCREENSPACE_EFFECTS,
LAYER_ULTRA_IMPORTANT_NOTIFICATIONS,
LAYER_LAST
} Layer;
@ -4498,6 +4520,11 @@ AABB draw_text(TextParams t)
return bounds;
}
float get_vertical_dist_between_lines(LoadedFont for_font, float text_scale)
{
return for_font.font_line_advance*text_scale*0.9f;
}
AABB draw_centered_text(TextParams t)
{
if(t.scale <= 0.01f) return (AABB){0};
@ -4505,7 +4532,10 @@ AABB draw_centered_text(TextParams t)
AABB text_aabb = draw_text(t);
t.dry_run = false;
Vec2 center_pos = t.pos;
t.pos = AddV2(center_pos, MulV2F(aabb_size(text_aabb), -0.5f));
LoadedFont *to_use = &default_font;
if(t.use_font) to_use = t.use_font;
//t.pos = AddV2(center_pos, MulV2F(aabb_size(text_aabb), -0.5f));
t.pos = V2(center_pos.x - aabb_size(text_aabb).x/2.0f, center_pos.y - get_vertical_dist_between_lines(*to_use, t.scale)/2.0f);
return draw_text(t);
}
@ -4750,6 +4780,7 @@ typedef struct PlacedWord
struct PlacedWord *prev;
MD_String8 text;
Vec2 lower_left_corner;
Vec2 size;
int line_index;
} PlacedWord;
@ -4759,32 +4790,35 @@ typedef struct
PlacedWord *last;
} PlacedWordList;
float get_vertical_dist_between_lines(LoadedFont for_font, float text_scale)
typedef enum
{
return for_font.font_line_advance*text_scale*0.9f;
}
JUST_LEFT,
JUST_CENTER,
} TextJustification;
PlacedWordList place_wrapped_words(MD_Arena *arena, MD_String8List words, float text_scale, float maximum_width, LoadedFont for_font)
PlacedWordList place_wrapped_words(MD_Arena *arena, MD_String8List words, float text_scale, float maximum_width, LoadedFont for_font, TextJustification just)
{
PlacedWordList to_return = {0};
MD_ArenaTemp scratch = MD_GetScratch(&arena, 1);
int current_line_index = 0;
{
Vec2 at_position = V2(0.0, 0.0);
Vec2 cur = at_position;
float space_size = character_width(for_font, (int)' ', text_scale);
float current_vertical_offset = 0.0f; // goes negative
int current_line_index = 0;
for(MD_String8Node *next_word = words.first; next_word; next_word = next_word->next)
for (MD_String8Node *next_word = words.first; next_word; next_word = next_word->next)
{
if(next_word->string.size == 0)
if (next_word->string.size == 0)
{
}
else
{
AABB word_bounds = draw_text((TextParams){true, next_word->string, V2(0.0, 0.0), .scale = text_scale});
AABB word_bounds = draw_text((TextParams){true, next_word->string, V2(0.0, 0.0), .scale = text_scale, .use_font = &for_font});
word_bounds.lower_right.x += space_size;
float next_x_position = cur.x + aabb_size(word_bounds).x;
if(next_x_position - at_position.x > maximum_width)
if (next_x_position - at_position.x > maximum_width)
{
current_vertical_offset -= get_vertical_dist_between_lines(for_font, text_scale); // the 1.1 is just arbitrary padding because it looks too crowded otherwise
cur = AddV2(at_position, V2(0.0f, current_vertical_offset));
@ -4795,6 +4829,7 @@ PlacedWordList place_wrapped_words(MD_Arena *arena, MD_String8List words, float
PlacedWord *new_placed = MD_PushArray(arena, PlacedWord, 1);
new_placed->text = next_word->string;
new_placed->lower_left_corner = cur;
new_placed->size = aabb_size(word_bounds);
new_placed->line_index = current_line_index;
MD_DblPushBack(to_return.first, to_return.last, new_placed);
@ -4802,6 +4837,51 @@ PlacedWordList place_wrapped_words(MD_Arena *arena, MD_String8List words, float
cur.x = next_x_position;
}
}
}
switch(just)
{
case JUST_LEFT:
break;
case JUST_CENTER:
{
//PlacedWord **by_line_index = MD_PushArray(scratch.arena, PlacedWord*, current_line_index);
for(int i = to_return.first->line_index; i <= to_return.last->line_index; i++)
{
PlacedWord *first_on_line = 0;
PlacedWord *last_on_line = 0;
for(PlacedWord *cur = to_return.first; cur; cur = cur->next)
{
if(cur->line_index == i)
{
if(first_on_line == 0)
{
first_on_line = cur;
}
}
else if(cur->line_index > i)
{
last_on_line = cur->prev;
assert(last_on_line->line_index == i);
break;
}
}
if(first_on_line && !last_on_line)
{
last_on_line = to_return.last;
}
float total_width = fabsf(first_on_line->lower_left_corner.x - (last_on_line->lower_left_corner.x + last_on_line->size.x));
float midpoint = total_width/2.0f;
for(PlacedWord *cur = first_on_line; cur != last_on_line->next; cur = cur->next)
{
cur->lower_left_corner.x = (cur->lower_left_corner.x - midpoint) + maximum_width/2.0f;
}
}
}
break;
}
MD_ReleaseScratch(scratch);
return to_return;
@ -4929,17 +5009,33 @@ typedef struct
struct { int key; IMState value; } *imui_state = 0;
bool imbutton_key(AABB button_aabb, float text_scale, MD_String8 text, int key, float dt, bool force_down)
typedef struct
{
IMState state = hmget(imui_state, key);
AABB button_aabb;
float text_scale;
MD_String8 text;
int key;
float dt;
bool force_down;
Layer layer;
LoadedFont *font;
} ImbuttonArgs;
bool imbutton_key(ImbuttonArgs args)
{
Layer layer = LAYER_UI;
LoadedFont *font = &default_font;
if(args.layer != LAYER_INVALID) layer = args.layer;
if(args.font) font = args.font;
IMState state = hmget(imui_state, args.key);
float raise = Lerp(0.0f, state.pressed_amount, 5.0f);
button_aabb.upper_left.y += raise;
button_aabb.lower_right.y += raise;
args.button_aabb.upper_left.y += raise;
args.button_aabb.lower_right.y += raise;
bool to_return = false;
float pressed_target = 0.5f;
if (has_point(button_aabb, mouse_pos))
if (has_point(args.button_aabb, mouse_pos))
{
if (pressed.mouse_down)
{
@ -4952,23 +5048,30 @@ bool imbutton_key(AABB button_aabb, float text_scale, MD_String8 text, int key,
}
if (pressed.mouse_up) state.is_being_pressed = false;
if (state.is_being_pressed || force_down) pressed_target = 0.0f;
if (state.is_being_pressed || args.force_down) pressed_target = 0.0f;
state.pressed_amount = Lerp(state.pressed_amount, dt*20.0f, pressed_target);
state.pressed_amount = Lerp(state.pressed_amount, args.dt*20.0f, pressed_target);
float button_alpha = Lerp(0.5f, state.pressed_amount, 1.0f);
if (aabb_is_valid(button_aabb))
if (aabb_is_valid(args.button_aabb))
{
draw_quad((DrawParams) { quad_aabb(button_aabb), IMG(image_white_square), blendalpha(WHITE, button_alpha), .layer = LAYER_UI, });
draw_centered_text((TextParams) { false, text, aabb_center(button_aabb), BLACK, text_scale, .clip_to = button_aabb, .do_clipping = true });
draw_quad((DrawParams) { quad_aabb(args.button_aabb), IMG(image_white_square), blendalpha(WHITE, button_alpha), .layer = layer, });
// don't use draw centered text here because it looks funny for some reason... I think it's because the vertical line advance of the font, used in draw_centered_text, is the wrong thing for a button like this
TextParams t = (TextParams) { false, args.text, aabb_center(args.button_aabb), BLACK, args.text_scale, .clip_to = args.button_aabb, .do_clipping = true, .layer = layer, .use_font = font};
t.dry_run = true;
AABB aabb = draw_text(t);
t.dry_run = false;
t.pos = SubV2(aabb_center(args.button_aabb), MulV2F(aabb_size(aabb), 0.5f));
draw_text(t);
}
hmput(imui_state, key, state);
hmput(imui_state, args.key, state);
return to_return;
}
#define imbutton(...) imbutton_key(__VA_ARGS__, __LINE__, unwarped_dt, false)
#define imbutton(...) imbutton_key((ImbuttonArgs){__VA_ARGS__, .key = __LINE__, .dt = unwarped_dt})
Quat rot_on_plane_to_quat(float rot)
{
@ -5565,16 +5668,35 @@ void flush_all_drawn_things(Vec3 light_dir, Vec3 cam_pos, Vec3 cam_facing, Vec3
}
}
typedef struct
{
float text_scale;
float width_in_pixels;
float text_width_in_pixels;
LoadedFont *font;
int lines_per_page;
int maximum_pages_from_ai;
} TextPlacementSettings;
TextPlacementSettings speech_bubble = {
.text_scale = 1.0f,
.width_in_pixels = 400.0f,
.text_width_in_pixels = 400.0f * 0.8f,
.font = &default_font,
.lines_per_page = 2,
.maximum_pages_from_ai = 2,
};
// Unsaid words are still there, so you gotta handle the animation homie
MD_String8List words_on_current_page(Entity *it)
MD_String8List words_on_current_page(Entity *it, TextPlacementSettings *settings)
{
MD_String8 last = last_said_sentence(it);
PlacedWordList placed = place_wrapped_words(frame_arena, split_by_word(frame_arena, last), BUBBLE_TEXT_SCALE, BUBBLE_TEXT_WIDTH_PIXELS, BUBBLE_FONT);
PlacedWordList placed = place_wrapped_words(frame_arena, split_by_word(frame_arena, last), settings->text_scale, settings->width_in_pixels, *settings->font, JUST_LEFT);
MD_String8List on_current_page = {0};
for(PlacedWord *cur = placed.first; cur; cur = cur->next)
{
if(cur->line_index / BUBBLE_LINES_PER_PAGE == it->cur_page_index)
if(cur->line_index / settings->lines_per_page == it->cur_page_index)
MD_S8ListPush(frame_arena, &on_current_page, cur->text);
}
@ -5583,9 +5705,9 @@ MD_String8List words_on_current_page(Entity *it)
//return place_wrapped_words(frame_arena, on_current_page, text_scale, aabb_size(placing_text_in).x, default_font);
}
MD_String8List words_on_current_page_without_unsaid(Entity *it)
MD_String8List words_on_current_page_without_unsaid(Entity *it, TextPlacementSettings *settings)
{
MD_String8List all_words = words_on_current_page(it);
MD_String8List all_words = words_on_current_page(it, settings);
int index = 0;
MD_String8List to_return = {0};
for(MD_String8Node *cur = all_words.first; cur; cur = cur->next)
@ -5875,17 +5997,137 @@ 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)
{
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);
MD_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 = MD_PushArray(persistent_arena, TextChunk, 1);
}
chunk_from_s8(to_say_chunk, new_to_say);
}
}
MD_String8List to_say = {0};
if(to_say_chunk != 0) to_say = split_by_word(frame_arena, TextChunkString8(*to_say_chunk));
MD_String8 cur_word = {0};
MD_String8Node *cur_word_node = 0;
double chars_said = cur_characters;
for(MD_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);
MD_String8Node *prev_next = cur_word_node->next;
cur_word_node->next = 0;
//MD_String8 without_unsaid = MD_S8ListJoin(frame_arena, to_say, &(MD_StringJoin){.mid = MD_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;
MD_String8 mission_text = FmtWithLint(frame_arena, "Your mission: %s %s", verbs[0], characters[gs.objective.who_to_kill].name);
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, MD_S8Lit("Accept"), .layer = LAYER_ANGEL_SCREEN, .font = &font_for_text_input))
{
Log("Accepting mission...\n");
}
}
else
{
draw_centered_text((TextParams){false, MD_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(text input drawing)
#ifdef DESKTOP
draw_quad((DrawParams){quad_at(V2(0,screen_size().y), screen_size()), IMG(image_white_square), blendalpha(BLACK, text_input_fade*0.3f), .layer = LAYER_UI_TEXTINPUT});
Vec2 edge_of_text = MulV2F(screen_size(), 0.5f);
float text_scale = 1.0f;
if(text_input_buffer_length > 0)
{
AABB bounds = draw_centered_text((TextParams){false, MD_S8(text_input_buffer, text_input_buffer_length), MulV2F(screen_size(), 0.5f), blendalpha(WHITE, text_input_fade), 1.0f, .use_font = &font_for_text_input, .layer = LAYER_UI_TEXTINPUT});
AABB bounds = draw_centered_text((TextParams){false, MD_S8(text_input_buffer, text_input_buffer_length), MulV2F(screen_size(), 0.5f), blendalpha(WHITE, text_input_fade), text_scale, .use_font = &font_for_text_input, .layer = LAYER_UI_TEXTINPUT});
edge_of_text = bounds.lower_right;
}
Vec2 cursor_center = V2(edge_of_text.x,screen_size().y/2.0f);
draw_quad((DrawParams){quad_centered(cursor_center, V2(3.0f, 80.0f)), IMG(image_white_square), blendalpha(WHITE, text_input_fade * (sinf((float)elapsed_time*8.0f)/2.0f + 0.5f)), .layer = LAYER_UI_TEXTINPUT});
draw_quad((DrawParams){quad_centered(cursor_center, V2(3.0f, get_vertical_dist_between_lines(font_for_text_input, text_scale))), IMG(image_white_square), blendalpha(WHITE, text_input_fade * (sinf((float)elapsed_time*8.0f)/2.0f + 0.5f)), .layer = LAYER_UI_TEXTINPUT});
#endif
Entity *cur_unread_entity = 0;
@ -5913,13 +6155,13 @@ void frame(void)
}
// dialog bubble rendering
const float text_scale = BUBBLE_TEXT_SCALE;
const float text_scale = speech_bubble.text_scale;
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(BUBBLE_WIDTH_PIXELS, BUBBLE_WIDTH_PIXELS);
Vec2 size = V2(speech_bubble.width_in_pixels, speech_bubble.width_in_pixels);
Vec2 bubble_center = AddV2(screen_pos, V2(-10.0f, 55.0f));
float dialog_alpha = clamp01(bubble_factor * it->dialog_fade);
bool unread = false;
@ -5935,7 +6177,7 @@ void frame(void)
blendalpha(WHITE, dialog_alpha),
.layer = LAYER_UI_FG,
});
MD_String8List words_to_say = words_on_current_page(it);
MD_String8List words_to_say = words_on_current_page(it, &speech_bubble);
if (unread)
{
draw_quad((DrawParams){
@ -5955,7 +6197,7 @@ void frame(void)
else
{
it->cur_page_index += 1;
if(words_on_current_page(it).node_count == 0)
if(words_on_current_page(it, &speech_bubble).node_count == 0)
{
// don't reset words_said_on_page because, even when the action is dismissed, the text for the last
// page of dialog should still linger
@ -5978,13 +6220,13 @@ void frame(void)
blendalpha(WHITE, it->loading_anim_in),
.layer = LAYER_UI_FG,
});
AABB placing_text_in = aabb_centered(AddV2(bubble_center, V2(0, 10.0f)), V2(BUBBLE_TEXT_WIDTH_PIXELS, size.y * 0.15f));
AABB placing_text_in = aabb_centered(AddV2(bubble_center, V2(0, 10.0f)), V2(speech_bubble.text_width_in_pixels, size.y * 0.15f));
dbgrect(placing_text_in);
MD_String8List to_draw = words_on_current_page_without_unsaid(it);
MD_String8List to_draw = words_on_current_page_without_unsaid(it, &speech_bubble);
if(to_draw.node_count != 0)
{
PlacedWordList placed = place_wrapped_words(frame_arena, to_draw, text_scale, aabb_size(placing_text_in).x, default_font);
PlacedWordList placed = place_wrapped_words(frame_arena, to_draw, text_scale, aabb_size(placing_text_in).x, default_font, JUST_LEFT);
// also called on npc response to see if it fits in the right amount of bubbles, if not tells AI how many words it has to trim its response by
// translate_words_by(placed, V2(placing_text_in.upper_left.x, placing_text_in.lower_right.y));
@ -6008,7 +6250,7 @@ void frame(void)
float speed_target = 1.0f;
gs.stopped_time = cur_unread_entity != 0 || (!gs.no_angel_screen);
gs.stopped_time = cur_unread_entity != 0;
if(gs.stopped_time) speed_target = 0.0f;
// pausing the game
speed_factor = Lerp(speed_factor, unwarped_dt*10.0f, speed_target);
@ -6137,11 +6379,12 @@ ISANERROR("Don't know how to do this stuff on this platform.")
MD_String8 parse_response = parse_chatgpt_response(scratch.arena, it, sentence_str, &out);
// check that it wraps in below two lines
PlacedWordList placed = place_wrapped_words(frame_arena, split_by_word(frame_arena, TextChunkString8(out.speech)), BUBBLE_TEXT_SCALE, BUBBLE_TEXT_WIDTH_PIXELS, BUBBLE_FONT);
TextPlacementSettings *to_wrap_to = &speech_bubble;
PlacedWordList placed = place_wrapped_words(frame_arena, split_by_word(frame_arena, TextChunkString8(out.speech)), to_wrap_to->text_scale, to_wrap_to->text_width_in_pixels, *to_wrap_to->font, JUST_LEFT);
int words_over_limit = 0;
for(PlacedWord *cur = placed.first; cur; cur = cur->next)
{
if(cur->line_index >= BUBBLE_LINES_PER_PAGE*AI_MAX_BUBBLE_PAGES_IN_OUTPUT) // the max number of lines of text on a bubble
if(cur->line_index >= to_wrap_to->lines_per_page*to_wrap_to->maximum_pages_from_ai) // the max number of lines of text on a bubble
{
words_over_limit += 1;
}
@ -6218,12 +6461,12 @@ ISANERROR("Don't know how to do this stuff on this platform.")
if (it->is_npc)
{
// character speech animation text input
if (true)
if (it->npc_kind != NPC_Angel)
{
MD_ArenaTemp scratch = MD_GetScratch(0, 0);
MD_String8List to_say = words_on_current_page(it);
MD_String8List to_say_without_unsaid = words_on_current_page_without_unsaid(it);
MD_String8List to_say = words_on_current_page(it, &speech_bubble);
MD_String8List to_say_without_unsaid = words_on_current_page_without_unsaid(it, &speech_bubble);
if(to_say.node_count > 0 && it->words_said_on_page < to_say.node_count)
{
if(cur_unread_entity == it)
@ -6660,12 +6903,22 @@ ISANERROR("Don't know how to do this stuff on this platform.")
{
const char *action = 0;
const char *action_argument = "Raphael";
if(gete(it->aiming_shotgun_at))
if(it->npc_kind == NPC_Daniel)
{
if (gete(it->aiming_shotgun_at))
{
action = "fire_shotgun";
} else {
}
else
{
action = "aim_shotgun";
}
}
else if(it->npc_kind == NPC_Angel)
{
action = "assign_gameplay_objective";
action_argument = "KILL Daniel";
}
char *rigged_dialog[] = {
"Repeated amounts of testing dialog overwhelmingly in support of the mulaney brothers",
};
@ -6777,7 +7030,8 @@ ISANERROR("Don't know how to do this stuff on this platform.")
float speed = 0.0f;
if(!gs.player->killed) speed = PLAYER_SPEED;
// velocity processing
if(!gs.no_angel_screen) speed = 0.0;
// velocity processing for player player movement
{
gs.player->last_moved = NormV2(movement);
Vec2 target_vel = MulV2F(movement, pixels_per_meter * speed);
@ -6912,79 +7166,10 @@ ISANERROR("Don't know how to do this stuff on this platform.")
if (having_errors)
{
Vec2 text_center = V2(screen_size().x / 2.0f, screen_size().y*0.8f);
draw_quad((DrawParams){centered_quad(text_center, V2(screen_size().x*0.8f, screen_size().y*0.1f)), IMG(image_white_square), blendalpha(BLACK, 0.5f), .layer = LAYER_UI_FG});
draw_centered_text((TextParams){false, MD_S8Lit("The AI server is having technical difficulties..."), text_center, WHITE, 1.0f });
draw_quad((DrawParams){centered_quad(text_center, V2(screen_size().x*0.8f, screen_size().y*0.1f)), IMG(image_white_square), blendalpha(BLACK, 0.5f), .layer = LAYER_ULTRA_IMPORTANT_NOTIFICATIONS});
draw_centered_text((TextParams){false, MD_S8Lit("The AI server is having technical difficulties..."), text_center, WHITE, 1.0f, .layer = LAYER_ULTRA_IMPORTANT_NOTIFICATIONS });
}
// angel screen
gs.no_angel_screen = true;
{
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);
Entity *angel_entity = 0;
ENTITIES_ITER(gs.entities)
{
if(it->is_npc && it->npc_kind == NPC_Angel)
{
assert(!angel_entity);
angel_entity = it;
}
}
assert(angel_entity);
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_UI_FG});
static MD_String8List to_say = {0};
static double cur_characters = 0;
if(to_say.node_count == 0)
{
to_say = split_by_word(persistent_arena, MD_S8Lit("You've been asleep for a long long time..."));
cur_characters = 0;
}
MD_String8 cur_word = {0};
MD_String8Node *cur_word_node = 0;
double chars_said = cur_characters;
for(MD_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)
{
cur_word = to_say.last->string;
cur_word_node = to_say.last;
}
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);
MD_String8Node *prev_next = cur_word_node->next;
cur_word_node->next = 0;
MD_String8 without_unsaid = MD_S8ListJoin(frame_arena, to_say, &(MD_StringJoin){.mid = MD_S8Lit(" ")});
cur_word_node->next = prev_next;
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});
draw_centered_text((TextParams){false, MD_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});
if(should_be_visible && pressed.interact)
{
begin_text_input();
}
}
// win screen
{

@ -94,15 +94,37 @@ typedef struct TextChunkList
TextChunk text;
} TextChunkList;
typedef struct GameplayObjective
{
ObjectiveVerb verb;
NpcKind who_to_kill;
} GameplayObjective;
typedef enum
{
ARG_CHARACTER,
ARG_OBJECTIVE,
} ActionArgumentKind;
typedef struct
{
ActionArgumentKind kind;
NpcKind targeting;
GameplayObjective objective;
} ActionArgument;
// returns ai understandable, human readable name, so not the enum name
MD_String8 action_argument_string(ActionArgument arg)
// returns ai understandable, human readable name, on the arena, so not the enum name
MD_String8 action_argument_string(MD_Arena *arena, ActionArgument arg)
{
return MD_S8CString(characters[arg.targeting].name);
switch(arg.kind)
{
case ARG_CHARACTER:
return FmtWithLint(arena, "%s", characters[arg.targeting].name);
break;
case ARG_OBJECTIVE:
return FmtWithLint(arena, "%s %s", verbs[arg.objective.verb], characters[arg.objective.who_to_kill].enum_name);
}
return (MD_String8){0};
}
typedef struct Action
@ -187,7 +209,7 @@ typedef struct
// text chunk must be a literal, not a pointer
// and this returns a s8 that points at the text chunk memory
#define TextChunkString8(t) MD_S8((MD_u8*)t.text, t.text_length)
#define TextChunkString8(t) MD_S8((MD_u8*)(t).text, (t).text_length)
#define TextChunkVArg(t) MD_S8VArg(TextChunkString8(t))
void chunk_from_s8(TextChunk *into, MD_String8 from)
@ -198,11 +220,6 @@ void chunk_from_s8(TextChunk *into, MD_String8 from)
into->text_length = (int)from.size;
}
typedef struct
{
NpcKind who_to_kill;
} GameObjective;
typedef struct Entity
{
bool exists;
@ -277,6 +294,8 @@ typedef struct GameState {
MD_String8 current_room_name; // the string is allocated on the level that is currently loaded
bool finished_reading_dying_dialog;
bool no_angel_screen;
bool assigned_objective;
GameplayObjective objective;
// processing may still occur after time has stopped on the gamestate,
bool stopped_time;
@ -323,6 +342,11 @@ 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_assign_gameplay_objective);
}
bool has_shotgun = it->npc_kind == NPC_Daniel;
if(has_shotgun)
{
@ -414,6 +438,21 @@ MD_String8 generate_chatgpt_prompt(MD_Arena *arena, GameState *gs, Entity *e, Ca
// @TODO unhardcode this, this will be a description of where the character is right now
AddFmt("You're currently standing in Daniel's farm's barn, a run-down structure that barely serves its purpose. Daniel's mighty protective of it though.\n");
if(e->npc_kind == NPC_Angel)
{
AddFmt("Acceptable verbs for assigning a gameplay objective:\n");
ARR_ITER(char*, verbs)
{
AddFmt("%s\n", *it);
}
AddFmt("\n");
AddFmt("The characters in the game you can use when you assign your gameplay objective:\n");
AddFmt("Raphael\n");
AddFmt("Daniel\n");
AddFmt("\n");
}
AddFmt("The actions you can perform, what they do, and the arguments they expect:\n");
AvailableActions can_perform;
fill_available_actions(gs, e, &can_perform);
@ -471,6 +510,9 @@ MD_String8 generate_chatgpt_prompt(MD_Arena *arena, GameState *gs, Entity *e, Ca
case ACT_end_conversation:
no_longer_wants_to_converse = true;
break;
case ACT_assign_gameplay_objective:
AddFmt("%.*s assigned a definitive game objective to %.*s", HUMAN(it->context.author_npc_kind), HUMAN(it->context.talking_to_kind));
break;
}
}
if(it->speech.text_length > 0)
@ -528,7 +570,8 @@ MD_String8 generate_chatgpt_prompt(MD_Arena *arena, GameState *gs, Entity *e, Ca
AddFmt("{");
AddFmt("\"speech\":\"%.*s\",", TextChunkVArg(it->speech));
AddFmt("\"action\":\"%s\",", actions[it->action_taken].name);
AddFmt("\"action_argument\":\"%.*s\",", MD_S8VArg(action_argument_string(it->action_argument)));
MD_String8 arg_str = action_argument_string(scratch.arena, it->action_argument);
AddFmt("\"action_argument\":\"%.*s\",", MD_S8VArg(arg_str));
AddFmt("\"target\":\"%s\"}", characters[it->context.talking_to_kind].name);
AddNewNode(MSG_ASSISTANT);
}
@ -548,6 +591,89 @@ MD_String8 get_field(MD_Node *parent, MD_String8 name)
return MD_ChildFromString(parent, name, 0)->first_child->string;
}
void parse_action_argument(MD_Arena *error_arena, MD_String8 *cur_error_message, ActionKind action, MD_String8 action_argument_str, ActionArgument *out)
{
assert(cur_error_message);
if(cur_error_message->size > 0) return;
MD_ArenaTemp scratch = MD_GetScratch(&error_arena, 1);
MD_String8 action_str = MD_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 (MD_S8Match(MD_S8CString(characters[i].name), action_argument_str, 0))
{
found_npc = true;
(*out).targeting = i;
}
}
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", MD_S8VArg(action_str), MD_S8VArg(action_argument_str));
}
}
else if (arg_is_gameplay_objective)
{
out->kind = ARG_OBJECTIVE;
MD_String8List split = MD_S8Split(scratch.arena, action_argument_str, 1, &MD_S8Lit(" "));
if (split.node_count != 2)
{
*cur_error_message = FmtWithLint(error_arena, "Gameplay objective must follow this format: `VERB ACTION`, with a space between the verb and the action. You gave a response with %d words in it, which isn't the required amount, 2", (int)split.node_count);
}
if (cur_error_message->size == 0)
{
MD_String8 verb = split.first->string;
bool found = false;
MD_String8List available_verbs = {0};
ARR_ITER_I(char *, verbs, verb_enum)
{
MD_S8ListPush(scratch.arena, &available_verbs, MD_S8CString(*it));
if (MD_S8Match(MD_S8CString(*it), verb, 0))
{
(*out).objective.verb = verb_enum;
found = true;
}
}
if (!found)
{
MD_String8 verbs_str = MD_S8ListJoin(scratch.arena, available_verbs, &(MD_StringJoin){.mid = MD_S8Lit(" ")});
*cur_error_message = FmtWithLint(error_arena, "The gameplay verb you provided '%.*s' doesn't match any of the gameplay verbs available to you: %.*s", MD_S8VArg(verb), MD_S8VArg(verbs_str));
}
}
if (cur_error_message->size == 0)
{
MD_String8 subject = split.first->next->string;
MD_String8 verb = split.first->string;
bool found_npc = false;
for (int i = 0; i < ARRLEN(characters); i++)
{
if (MD_S8Match(MD_S8CString(characters[i].name), subject, 0))
{
found_npc = true;
(*out).objective.who_to_kill = i;
}
}
if (!found_npc)
{
*cur_error_message = FmtWithLint(error_arena, "Argument for gameplay verb `%.*s` you gave is `%.*s`, which doesn't exist in the game so is invalid", MD_S8VArg(verb), MD_S8VArg(subject));
}
}
}
else
{
assert(false); // don't know how to parse the argument string for this kind of action...
}
MD_ReleaseScratch(scratch);
}
// if returned string has size greater than 0, it's the error message. Allocated
// on arena passed into it or in constant memory
@ -644,29 +770,7 @@ MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 action_
{
if(actions[out->kind].takes_argument)
{
// @TODO refactor into, action argument kinds and they parse into different action argument types
bool arg_is_character = out->kind == ACT_join || out->kind == ACT_aim_shotgun || out->kind == ACT_end_conversation;
if(arg_is_character)
{
bool found_npc = false;
for(int i = 0; i < ARRLEN(characters); i++)
{
if(MD_S8Match(MD_S8CString(characters[i].name), action_argument_str, 0))
{
found_npc = true;
out->argument.targeting = i;
}
}
if(!found_npc)
{
error_message = FmtWithLint(arena, "Argument for action `%.*s` you gave is `%.*s`, which doesn't exist in the game so is invalid", MD_S8VArg(action_str), MD_S8VArg(action_argument_str));
}
}
else
{
assert(false); // don't know how to parse the argument string for this kind of action...
}
parse_action_argument(arena,&error_message, out->kind, action_argument_str, &out->argument);
}
}
}

@ -1,4 +1,7 @@
Urgent:
- White sidebars on angel's speech that recede away.
- Mystical particles that float upwards.
- Gradient png on the bottom that has more alpha, controlled by same parameter as white sidebars.
- Fix everything about the shotgun, put shotgun away. Make them understand they're holding their shotgun more. REALLY look into characters not being able to shoot eachother
- On startup if devtools print the estimated cost per generation request of the worst offending character
- simple room transition system for angel/real map interop, angel room

@ -19,11 +19,6 @@
#define CAM_VERTICAL_TO_HORIZONTAL_RATIO 0.95f
#define DIALOG_FADE_TIME 3.0f
#define BUBBLE_WIDTH_PIXELS 400.0f
#define BUBBLE_TEXT_WIDTH_PIXELS (BUBBLE_WIDTH_PIXELS*0.8f)
#define BUBBLE_TEXT_SCALE 1.0f
#define BUBBLE_FONT default_font
#define BUBBLE_LINES_PER_PAGE 2
#define AI_MAX_BUBBLE_PAGES_IN_OUTPUT 2
#define ARENA_SIZE (1024*1024*20)

Loading…
Cancel
Save