Compare commits

..

2 Commits

@ -6,6 +6,8 @@ RPG GPT. Short experience
# Important Building Steps and Contribution Notes # Important Building Steps and Contribution Notes
Every time you checkin/clone the project, you have to unzip art.blend... If this is annoying to you, make a git hook Every time you checkin/clone the project, you have to unzip art.blend... If this is annoying to you, make a git hook
When editing Exporter.py in either the blender editor, or in a text editor in the repo, you have to continually make sure blender's internal version of the script doesn't go out of date with the actual script on disk, by either saving consistently from blender to disk if you're editing from blender, or by reloading from disk in the blend file before each commit.
Be very cautious about committing a change to any large asset files, i.e the art.blend and png files. Every time you do so, even if you change one little thing like moving the player somewhere, you copy the entire file in git lfs, ballooning the storage usage of the git project on the remote. So just try to minimize edits to those big files. Be very cautious about committing a change to any large asset files, i.e the art.blend and png files. Every time you do so, even if you change one little thing like moving the player somewhere, you copy the entire file in git lfs, ballooning the storage usage of the git project on the remote. So just try to minimize edits to those big files.
You must clone with git lfs is, and download git lfs files in this repository. If you don't know what that is, google it You must clone with git lfs is, and download git lfs files in this repository. If you don't know what that is, google it

@ -109,7 +109,7 @@ def ensure_tex_saved_and_get_name(o) -> str:
if img_obj.packed_file: if img_obj.packed_file:
img_obj.save(filepath=bpy.path.abspath(save_to)) img_obj.save(filepath=bpy.path.abspath(save_to))
else: else:
assert img_obj.filepath != "", f"{img_obj.filepath} in mesh {mesh_name} Isn't there but should be, as it has no packed image" assert img_obj.filepath != "", f"filepath '{img_obj.filepath}' in mesh {mesh_name} Isn't there but should be, as it has no packed image"
old_path = bpy.path.abspath(img_obj.filepath) old_path = bpy.path.abspath(img_obj.filepath)
if not is_file_in_project(old_path): if not is_file_in_project(old_path):
print(f"Image {image_filename} has filepath {img_obj.filepath}, outside of the current directory. So we're copying it baby. Hoo-rah!") print(f"Image {image_filename} has filepath {img_obj.filepath}, outside of the current directory. So we're copying it baby. Hoo-rah!")
@ -123,11 +123,12 @@ def ensure_tex_saved_and_get_name(o) -> str:
return image_filename return image_filename
def object_in_level(o):
return o.users_collection[0].name == "Level" or (o.users_collection[0] in D.collections["Level"].children_recursive)
# meshes can either be Meshes, or Armatures. Armatures contain all mesh data to draw it, and any anims it has def object_in_collection(o, collection):
"""Probably only works for root level collections"""
return o.users_collection[0].name == collection or (o.users_collection[0] in D.collections[collection].children_recursive)
# meshes can either be Meshes, or Armatures. Armatures contain all mesh data to draw it, and any anims it has
for o in D.objects: for o in D.objects:
if o.hide_get(): continue if o.hide_get(): continue
if o.type == "MESH": if o.type == "MESH":
@ -135,7 +136,7 @@ for o in D.objects:
mesh_object = o mesh_object = o
o = o.parent o = o.parent
object_transform_info = (mesh_name, mapping @ o.location, o.rotation_euler, o.scale) object_transform_info = (mesh_name, mapping @ o.location, o.rotation_euler, o.scale)
if object_in_level(o): if object_in_collection(o, "Level") or object_in_collection(o, "PlacedEntities"):
assert False, "Cannot put armatures in the level. The level is for static placed meshes. For dynamic entities, you put them outside of the level collection, their entity kind is encoded, and the game code decides how to draw them" assert False, "Cannot put armatures in the level. The level is for static placed meshes. For dynamic entities, you put them outside of the level collection, their entity kind is encoded, and the game code decides how to draw them"
else: else:
pass pass
@ -289,20 +290,20 @@ for o in D.objects:
object_transform_info = (mesh_name, mapping @ o.location, o.rotation_euler, o.scale) object_transform_info = (mesh_name, mapping @ o.location, o.rotation_euler, o.scale)
if object_in_level(o) and mesh_name == "CollisionCube": if object_in_collection(o, "Level") and mesh_name == "CollisionCube":
collision_cubes.append((o.location, o.dimensions)) collision_cubes.append((o.location, o.dimensions))
else: else:
if object_in_level(o): if object_in_collection(o, "Level"):
print(f"Object {o.name} has mesh name {o.to_mesh().name}") print(f"Object {o.name} has mesh name {o.to_mesh().name}")
assert(o.rotation_euler.order == 'XYZ') assert(o.rotation_euler.order == 'XYZ')
level_object_data.append(object_transform_info) level_object_data.append(object_transform_info)
else: elif object_in_collection(o, "PlacedEntities"):
placed_entities.append((o.name,) + object_transform_info) placed_entities.append((o.name,) + object_transform_info)
if mesh_name in saved_meshes: if mesh_name in saved_meshes:
continue continue
saved_meshes.add(mesh_name) saved_meshes.add(mesh_name)
print(f"Mesh name {mesh_name} in level {object_in_level(o)} collections {o.users_collection}") print(f"""Mesh name {mesh_name} in level {object_in_collection(o, "Level")} collections {o.users_collection}""")
image_filename = ensure_tex_saved_and_get_name(o) image_filename = ensure_tex_saved_and_get_name(o)
assert(mesh_name != LEVEL_EXPORT_NAME) assert(mesh_name != LEVEL_EXPORT_NAME)

BIN
art/Shotgun.png (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

@ -21,6 +21,7 @@ typedef struct
} ActionInfo; } ActionInfo;
ActionInfo actions[] = { ActionInfo actions[] = {
#define NO_ARGUMENT .argument_description = "Takes no argument", .takes_argument = false #define NO_ARGUMENT .argument_description = "Takes no argument", .takes_argument = false
#define ARGUMENT(desc) .argument_description = desc, .takes_argument = true
{ {
.name = "none", .name = "none",
.description = "Do nothing", .description = "Do nothing",
@ -29,13 +30,23 @@ ActionInfo actions[] = {
{ {
.name = "join", .name = "join",
.description = "Joins somebody else's party, so you follow them everywhere", .description = "Joins somebody else's party, so you follow them everywhere",
.argument_description = "Expects the argument to be who you're joining", ARGUMENT("Expects the argument to be who you're joining"),
}, },
{ {
.name = "leave", .name = "leave",
.description = "Leave the party you're in right now", .description = "Leave the party you're in right now",
NO_ARGUMENT, NO_ARGUMENT,
}, },
{
.name = "aim_shotgun",
.description = "Aims your shotgun at the specified target, preparing you to fire at them and threatening their life.",
ARGUMENT("Expects the argument to be the name of the person you're aiming at, they must be nearby"),
},
{
.name = "fire_shotgun",
.description = "Fires your shotgun at the current target, killing the target.",
NO_ARGUMENT,
},
}; };
typedef enum typedef enum

@ -1644,17 +1644,23 @@ MD_String8 is_action_valid(MD_Arena *arena, Entity *from, Action a)
{ {
error_message = FmtWithLint(arena, "You can't join somebody, you're already in %s's party", characters[gete(from->joined)->npc_kind].name); error_message = FmtWithLint(arena, "You can't join somebody, you're already in %s's party", characters[gete(from->joined)->npc_kind].name);
} }
if(error_message.size == 0 && a.kind == ACT_fire_shotgun && gete(from->aiming_shotgun_at) == 0)
{
error_message = MD_S8Lit("You can't fire your shotgun without aiming it first");
}
bool target_is_character = a.kind == ACT_join || a.kind == ACT_fire_shotgun;
if(error_message.size == 0 && a.kind == ACT_join) if(error_message.size == 0 && target_is_character)
{ {
bool talk_to_valid = false; bool arg_valid = false;
BUFF_ITER(NpcKind, &talk) BUFF_ITER(NpcKind, &talk)
{ {
if(*it == a.argument.targeting) talk_to_valid = true; if(*it == a.argument.targeting) arg_valid = true;
} }
if(talk_to_valid == false) if(arg_valid == false)
{ {
error_message = FmtWithLint(arena, "Your action_argument for who to join, %s, is either invalid (you can't join nobody) or it's not an NPC that's near you right now.", characters[a.argument.targeting].name); error_message = FmtWithLint(arena, "Your action_argument for who this action should be directed at, %s, is either invalid (you can't operate on nobody) or it's not an NPC that's near you right now.", characters[a.argument.targeting].name);
} }
} }
@ -1719,6 +1725,12 @@ void cause_action_side_effects(Entity *from, Action a)
{ {
from->joined = (EntityRef){0}; from->joined = (EntityRef){0};
} }
if(a.kind == ACT_aim_shotgun)
{
Entity *target = get_targeted(from, a.argument.targeting);
assert(target); // error checked in is_action_valid
from->aiming_shotgun_at = frome(target);
}
MD_ReleaseScratch(scratch); MD_ReleaseScratch(scratch);
} }
@ -3084,6 +3096,7 @@ Armature *armatures[] = {
Mesh mesh_player = {0}; Mesh mesh_player = {0};
Mesh mesh_simple_worm = {0}; Mesh mesh_simple_worm = {0};
Mesh mesh_shotgun = {0};
void stbi_flip_into_correct_direction(bool do_it) void stbi_flip_into_correct_direction(bool do_it)
{ {
@ -3139,6 +3152,9 @@ void init(void)
binary_file = MD_LoadEntireFile(frame_arena, MD_S8Lit("assets/exported_3d/ExportedWithAnims.bin")); binary_file = MD_LoadEntireFile(frame_arena, MD_S8Lit("assets/exported_3d/ExportedWithAnims.bin"));
mesh_player = load_mesh(persistent_arena, binary_file, MD_S8Lit("ExportedWithAnims.bin")); mesh_player = load_mesh(persistent_arena, binary_file, MD_S8Lit("ExportedWithAnims.bin"));
binary_file = MD_LoadEntireFile(frame_arena, MD_S8Lit("assets/exported_3d/ShotgunMesh.bin"));
mesh_shotgun = load_mesh(persistent_arena, binary_file, MD_S8Lit("ShotgunMesh.bin"));
binary_file = MD_LoadEntireFile(frame_arena, MD_S8Lit("assets/exported_3d/ArmatureExportedWithAnims.bin")); 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")); player_armature = load_armature(persistent_arena, binary_file, MD_S8Lit("ArmatureExportedWithAnims.bin"));
@ -3750,6 +3766,11 @@ Vec3 ray_intersect_plane(Vec3 ray_point, Vec3 ray_vector, Vec3 plane_point, Vec3
float d = DotV3(plane_point, MulV3F(plane_normal, -1.0f)); float d = DotV3(plane_point, MulV3F(plane_normal, -1.0f));
float denom = DotV3(plane_normal, ray_vector); float denom = DotV3(plane_normal, ray_vector);
if(fabsf(denom) <= 1e-4f)
{
// also could mean doesn't intersect plane
return plane_point;
}
assert(fabsf(denom) > 1e-4f); // avoid divide by zero assert(fabsf(denom) > 1e-4f); // avoid divide by zero
float t = -(DotV3(plane_normal, ray_point) + d) / DotV3(plane_normal, ray_vector); float t = -(DotV3(plane_normal, ray_point) + d) / DotV3(plane_normal, ray_vector);
@ -4690,13 +4711,18 @@ bool imbutton_key(AABB button_aabb, float text_scale, MD_String8 text, int key,
#define imbutton(...) imbutton_key(__VA_ARGS__, __LINE__, unwarped_dt, false) #define imbutton(...) imbutton_key(__VA_ARGS__, __LINE__, unwarped_dt, false)
Quat rot_on_plane_to_quat(float rot)
{
return QFromAxisAngle_RH(V3(0,1,0), AngleRad(-rot));
}
Transform entity_transform(Entity *e) Transform entity_transform(Entity *e)
{ {
// Models must face +X in blender. This is because, in the 2d game coordinate system, // Models must face +X in blender. This is because, in the 2d game coordinate system,
// a zero degree 2d rotation means you're facing +x, and this is how it is in the game logic. // a zero degree 2d rotation means you're facing +x, and this is how it is in the game logic.
// The rotation is negative for some reason that I'm not quite sure about though, something about // The rotation is negative for some reason that I'm not quite sure about though, something about
// the handedness of the 3d coordinate system not matching the handedness of the 2d coordinate system // the handedness of the 3d coordinate system not matching the handedness of the 2d coordinate system
Quat entity_rot = QFromAxisAngle_RH(V3(0,1,0), AngleRad(-e->rotation)); Quat entity_rot = rot_on_plane_to_quat(e->rotation);
return (Transform){.offset = AddV3(plane_point(e->pos), V3(0,0,0)), .rotation = entity_rot, .scale = V3(1, 1, 1)}; return (Transform){.offset = AddV3(plane_point(e->pos), V3(0,0,0)), .rotation = entity_rot, .scale = V3(1, 1, 1)};
/* /*
@ -5479,6 +5505,15 @@ void frame(void)
to_use->go_to_animation = MD_S8Lit("Idle"); to_use->go_to_animation = MD_S8Lit("Idle");
draw_thing((DrawnThing){.armature = to_use, .t = draw_with, .outline = gete(gs.player->interacting_with) == it}); draw_thing((DrawnThing){.armature = to_use, .t = draw_with, .outline = gete(gs.player->interacting_with) == it});
if(gete(it->aiming_shotgun_at))
{
Transform shotgun_t = draw_with;
shotgun_t.offset.y += 2.0f;
shotgun_t.scale = V3(3,3,3);
shotgun_t.rotation = rot_on_plane_to_quat(AngleOfV2(SubV2(gete(it->aiming_shotgun_at)->pos, it->pos)));
draw_thing((DrawnThing){.mesh = &mesh_shotgun, .t = shotgun_t});
}
} }
} }
} }
@ -6395,14 +6430,14 @@ ISANERROR("Don't know how to do this stuff on this platform.")
if (it->memories_last->context.talking_to_kind == it->npc_kind) if (it->memories_last->context.talking_to_kind == it->npc_kind)
//if (it->memories_last->context.author_npc_kind != it->npc_kind) //if (it->memories_last->context.author_npc_kind != it->npc_kind)
{ {
const char *action = "none"; const char *action = "aim_shotgun";
char *rigged_dialog[] = { char *rigged_dialog[] = {
"Repeated amounts of testing dialog overwhelmingly in support of the mulaney brothers", "Repeated amounts of testing dialog overwhelmingly in support of the mulaney brothers",
}; };
char *next_dialog = rigged_dialog[it->times_talked_to % ARRLEN(rigged_dialog)]; char *next_dialog = rigged_dialog[it->times_talked_to % ARRLEN(rigged_dialog)];
char *target = characters[it->memories_last->context.author_npc_kind].name; char *target = characters[it->memories_last->context.author_npc_kind].name;
target = characters[NPC_Player].name; target = characters[NPC_Player].name;
ai_response = FmtWithLint(frame_arena, "{\"target\": \"%s\", \"action\": \"%s\", \"speech\": \"%s\"}", target, action, next_dialog); ai_response = FmtWithLint(frame_arena, "{\"target\": \"%s\", \"action\": \"%s\", \"action_arg\": \"Raphael\", \"speech\": \"%s\"}", target, action, next_dialog);
#ifdef DESKTOP #ifdef DESKTOP
it->times_talked_to += 1; it->times_talked_to += 1;
#endif #endif
@ -6433,6 +6468,7 @@ ISANERROR("Don't know how to do this stuff on this platform.")
{ {
assert(succeeded); assert(succeeded);
assert(error_message.size == 0); assert(error_message.size == 0);
assert(is_action_valid(frame_arena, it, a));
perform_action(&gs, it, a); perform_action(&gs, it, a);
} }
else else

@ -229,6 +229,7 @@ typedef struct Entity
// npcs // npcs
NpcKind npc_kind; NpcKind npc_kind;
EntityRef joined; EntityRef joined;
EntityRef aiming_shotgun_at;
float target_rotation; // turns towards this angle in conversation float target_rotation; // turns towards this angle in conversation
bool being_hovered; bool being_hovered;
bool perceptions_dirty; bool perceptions_dirty;
@ -255,6 +256,9 @@ typedef struct Entity
int gen_request_id; int gen_request_id;
Vec2 target_goto; Vec2 target_goto;
// character // character
bool waiting_on_speech_with_somebody; bool waiting_on_speech_with_somebody;
EntityRef interacting_with; // for drawing outline on maybe interacting with somebody EntityRef interacting_with; // for drawing outline on maybe interacting with somebody
@ -316,6 +320,19 @@ void fill_available_actions(GameState *gs, Entity *it, AvailableActions *a)
{ {
BUFF_APPEND(a, ACT_join) BUFF_APPEND(a, ACT_join)
} }
bool has_shotgun = it->npc_kind == NPC_Daniel;
if(has_shotgun)
{
if(gete_specified(gs, it->aiming_shotgun_at))
{
BUFF_APPEND(a, ACT_fire_shotgun);
}
else
{
BUFF_APPEND(a, ACT_aim_shotgun);
}
}
} }
@ -408,6 +425,14 @@ MD_String8 generate_chatgpt_prompt(MD_Arena *arena, GameState *gs, Entity *e, Ca
// Needs better handling of when you leave, because the person you were following died. Maybe entities don't die anymore? // Needs better handling of when you leave, because the person you were following died. Maybe entities don't die anymore?
AddFmt("%s left their party\n", characters[it->context.author_npc_kind].name); AddFmt("%s left their party\n", characters[it->context.author_npc_kind].name);
} }
else if(it->action_taken == ACT_aim_shotgun)
{
AddFmt("%s aimed their shotgun at %s\n", characters[it->context.author_npc_kind].name, characters[it->action_argument.targeting].name);
}
else
{
assert(false);
}
} }
if(it->speech.text_length > 0) if(it->speech.text_length > 0)
{ {
@ -567,7 +592,10 @@ MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 action_
{ {
if(actions[out->kind].takes_argument) if(actions[out->kind].takes_argument)
{ {
if(out->kind == ACT_join) // @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;
if(arg_is_character)
{ {
bool found_npc = false; bool found_npc = false;
for(int i = 0; i < ARRLEN(characters); i++) for(int i = 0; i < ARRLEN(characters); i++)

@ -28,7 +28,7 @@
#ifdef DEVTOOLS #ifdef DEVTOOLS
// server url cannot have trailing slash // server url cannot have trailing slash
//#define MOCK_AI_RESPONSE #define MOCK_AI_RESPONSE
#define SERVER_DOMAIN "localhost" #define SERVER_DOMAIN "localhost"
#define SERVER_PORT 8090 #define SERVER_PORT 8090
#define IS_SERVER_SECURE 0 #define IS_SERVER_SECURE 0

Loading…
Cancel
Save