Compare commits

...

6 Commits

@ -4,6 +4,8 @@ A fantasy western RPG with an immersive and natural dynamic dialogue system powe
![Western Frontier](https://upload.wikimedia.org/wikipedia/commons/thumb/3/32/Distribution_of_US_Rural_Population_during_1900.pdf/page1-1280px-Distribution_of_US_Rural_Population_during_1900.pdf.jpg)
# Important Building Steps and Contribution Notes
If you add new devtools functionality like a new keyboard button binding that prints stuff to help with debugging, or flycam features, or anything, MAKE SURE you add a description of that functionality and how to access it in the function make_devtools_help() so it's just clear what debugging tools there are. Document it please.
Every time you checkin/clone the project, make sure to call `blender_export.bat` at least once! This will auto-extract `art\art.blend` and run `art\Exporter.py`, thereby baking, validating, and exporting all the assets and the level.
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.

@ -5,14 +5,14 @@ import shutil
import struct
from mathutils import *; from math import *
from bpy_extras.io_utils import (axis_conversion)
from dataclasses import dataclass
C = bpy.context
D = bpy.data
LEVEL_EXPORT_NAME = "level"
ROOMS_EXPORT_NAME = "rooms"
EXPORT_DIRECTORY = "../assets/exported_3d"
print("\n\nLet's get it started")
if os.path.exists(bpy.path.abspath(f"//{EXPORT_DIRECTORY}")):
shutil.rmtree(bpy.path.abspath(f"//{EXPORT_DIRECTORY}"))
@ -63,11 +63,6 @@ def normalize_joint_weights(weights):
return result
# for the level.bin
level_object_data = []
collision_cubes = []
placed_entities = []
saved_meshes = set()
mapping = axis_conversion(
from_forward = "Y",
from_up = "Z",
@ -76,10 +71,6 @@ mapping = axis_conversion(
)
mapping.resize_4x4()
with open(bpy.path.abspath(f"//{EXPORT_DIRECTORY}/shorttest.bin"), "wb") as f:
for i in range(4):
write_u16(f, i)
project_directory = bpy.path.abspath("//")
def is_file_in_project(file_path):
file_name = os.path.basename(file_path)
@ -90,22 +81,24 @@ def is_file_in_project(file_path):
saved_images = set()
def ensure_tex_saved_and_get_name(o) -> str:
"""returns the path to the mesh's texture's png in the exported directory"""
"""returns the name of the mesh's texture's png in the exported directory, of the current object"""
assert o.type == "MESH", f"Object {o.name} isn't a mesh and is attempting to be saved as such"
mesh_name = o.to_mesh().name
# find the image object in the material of the object
img_obj = None
assert len(o.material_slots) == 1, f"Don't know which material slot to pick from in mesh {mesh_name} object {o.name}"
assert len(o.material_slots) == 1, f"Don't know which material slot to pick from in mesh {mesh_name} object {o.name}, there must only be one material slot on every mesh"
mat = o.material_slots[0]
for node in mat.material.node_tree.nodes:
if node.type == "TEX_IMAGE":
assert img_obj == None, f"Material on object {o.name} has more than one image node in its material, so I don't know which image node to use in the game engine. Make sure materials only use one image node"
img_obj = node.image
break
assert img_obj, f"Mesh {mesh_name} in its material doesn't have an image object"
image_filename = f"{img_obj.name}.png"
if image_filename in saved_images:
pass
else:
save_to = f"//{EXPORT_DIRECTORY}/{image_filename}"
print(f"Saving image {image_filename} to {bpy.path.abspath((save_to))}...")
if img_obj.packed_file:
img_obj.save(filepath=bpy.path.abspath(save_to))
else:
@ -123,33 +116,45 @@ def ensure_tex_saved_and_get_name(o) -> str:
return image_filename
def export_armatures():
print("Exporting armatures...")
exported = D.collections.get("Exported")
assert exported != None, f"No exported collection named 'Exported' in scene, very bad! Did everything to get exported get deleted?"
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)
armatures_collection = exported.children.get("Armatures")
assert armatures_collection != None, f"No child named 'Armatures' on the exported collection, this is required"
# 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:
if o.hide_get(): continue
A = armatures_collection
armatures_to_export = []
for o in A.objects:
if o.type == "MESH":
if o.parent and o.parent.type == "ARMATURE":
mesh_object = o
o = o.parent
object_transform_info = (mesh_name, mapping @ o.location, o.rotation_euler, o.scale)
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 o.parent, f"Mesh named {o.name} without parent in armatures collection is invalid, only armatures allowed in there!"
assert o.parent.type == "ARMATURE", f"Mesh named {o.name} isn't an armature, and isn't a child of an armature. This isn't allowed."
elif o.type == "ARMATURE":
armatures_to_export.append(o)
else:
pass
#placed_entities.append((mesh_object.name,) + object_transform_info)
armature_name = o.data.name
output_filepath = bpy.path.abspath(f"//{EXPORT_DIRECTORY}/{armature_name}.bin")
image_filename = ensure_tex_saved_and_get_name(mesh_object)
print(f"Exporting armature with image filename {image_filename} to {output_filepath}")
assert False, f"Object named {o.name} is of an unknown type '{o.type}', only objects that are Meshes or Armature are allowed in the armatures collection"
for armature_object in armatures_to_export:
# get the body mesh object
body_object = None
for c in armature_object.children:
if c.name.startswith("Body"):
assert body_object == None, f"On object {armature_object.name}, more than one child has a name that starts with 'Body', specifically '{body_object.name}' and '{c.name}', only one child's name is allowed to start with 'Body' in this object."
body_object = c
# initialize important variables
mesh_object = body_object
armature_object = armature_object
output_filepath = bpy.path.abspath(f"//{EXPORT_DIRECTORY}/{armature_object.name}.bin")
mesh_image_filename = ensure_tex_saved_and_get_name(mesh_object)
#print(f"Exporting armature with image filename {mesh_image_filename} to {output_filepath}")
with open(output_filepath, "wb") as f:
write_b8(f, True)
write_string(f, image_filename)
write_b8(f, True) # first byte is true if it's an armature file
write_string(f, mesh_image_filename)
bones_in_armature = []
for b in o.data.bones:
for b in armature_object.data.bones:
bones_in_armature.append(b)
# the inverse model space pos of the bones
@ -177,15 +182,15 @@ for o in D.objects:
# it's very important that the array of pose bones contains the same amount of bones
# as there are in the edit bones. Because the edit bones are exported, etc etc. Cowabunga!
assert(len(o.pose.bones) == len(bones_in_armature))
assert(len(armature_object.pose.bones) == len(bones_in_armature))
armature = o
armature = armature_object
anims = []
assert armature.animation_data, "Armatures are assumed to have an animation right now"
for track in armature.animation_data.nla_tracks:
for strip in track.strips:
anims.append(strip.action)
print(f"Writing {len(anims)} animations")
#print(f"Writing {len(anims)} animations")
write_u64(f, len(anims))
for animation in anims:
write_string(f, animation.name)
@ -195,7 +200,7 @@ for o in D.objects:
startFrame = int(animation.frame_range.x)
endFrame = int(animation.frame_range.y)
total_frames = (endFrame - startFrame) + 1 # the end frame is inclusive
print(f"Exporting animation {animation.name} with {total_frames} frames")
#print(f"Exporting animation {animation.name} with {total_frames} frames")
write_u64(f, total_frames)
@ -205,8 +210,8 @@ for o in D.objects:
bpy.context.scene.frame_set(frame)
write_f32(f, time_through_this_frame_occurs_at)
for pose_bone_i in range(len(o.pose.bones)):
pose_bone = o.pose.bones[pose_bone_i]
for pose_bone_i in range(len(armature_object.pose.bones)):
pose_bone = armature_object.pose.bones[pose_bone_i]
# in the engine, it's assumed that the poses are in the same order as the bones
# they're referring to. This checks that that is the case.
@ -218,11 +223,7 @@ for o in D.objects:
parent_space_pose = pose_bone.parent.matrix.inverted() @ pose_bone.matrix
else:
parent_space_pose = mapping @ pose_bone.matrix
#parent_space_pose = pose_bone.matrix
#print("parent_space_pose of the bone with no parent:")
#print(parent_space_pose)
#parent_space_pose = mapping @ pose_bone.matrix
translation = parent_space_pose.to_translation()
rotation = parent_space_pose.to_quaternion()
scale = parent_space_pose.to_scale()
@ -242,7 +243,7 @@ for o in D.objects:
vertices = []
armature = o
armature = armature_object
for polygon in mesh.polygons:
if len(polygon.loop_indices) == 3:
for loopIndex in polygon.loop_indices:
@ -283,44 +284,149 @@ for o in D.objects:
for i in range(4):
write_f32(f, jointWeights[i])
vertex_i += 1
print(f"Wrote {len(vertices)} vertices")
#print(f"Wrote {len(vertices)} vertices")
print("Success!")
def collect_and_validate_mesh_objects(collection_with_only_mesh_objects):
to_return = []
for o in collection_with_only_mesh_objects.all_objects:
assert o.type == "MESH", f"The collection '{collection_with_only_mesh_objects.name}' is assumed to contain only mesh objects but the object '{o.name}' which is a child of it isn't a mesh object"
to_return.append(o)
return to_return
def get_startswith(name_of_overarching_thing, iterable, starts_with):
"""
Gets the thing in iterable that starts with starts with, and makes sure there's only *one* thing that starts with starts with
name_of_overarching_thing is for the error message, for reporting in what collection things went wrong in.
"""
to_return = None
for thing in iterable:
if thing.name.startswith(starts_with):
assert to_return == None, f"Duplicate thing that starts with '{starts_with}' found in {name_of_overarching_thing} called {thing.name}"
to_return = thing
assert to_return != None, f"Couldn't find thing that starts with '{starts_with}' as a child of '{name_of_overarching_thing}', but one is required"
return to_return
def no_hidden(objects):
to_return = []
for o in objects:
if not o.hide_get():
to_return.append(o)
return to_return
def export_meshes_and_levels():
print("Exporting meshes and levels")
exported = D.collections.get("Exported")
assert exported != None, f"No exported collection named 'Exported' in scene, very bad! Did everything to get exported get deleted?"
mesh_names_to_export = set()
meshes_to_export = []
# add the collection 'Meshes' objects to mesh_objects_to_export
if True:
meshes = get_startswith("Exported", exported.children, "Meshes")
to_export = collect_and_validate_mesh_objects(meshes)
for m in no_hidden(to_export):
mesh_names_to_export.add(m.name)
meshes_to_export.append(m)
# export each level: its placed entities, placed meshes, and add to meshes_to_export and mesh_names_to_export. Those must be exported to their correct paths for the rooms to point at valid data.
rooms = exported.children.get("Rooms")
assert rooms != None, f"No child named 'Rooms' on the exported collection, this is required"
with open(bpy.path.abspath(f"//{EXPORT_DIRECTORY}/{ROOMS_EXPORT_NAME}.bin"), "wb") as f:
write_u64(f, len(rooms.children))
for room_collection in rooms.children:
write_string(f, room_collection.name) # the name of the room is the name of the room collection
# placed meshes (exported mesh name (which is the object's name), location, rotation, scale)
placed_meshes = []
if True:
meshes_collection = get_startswith(room_collection.name, room_collection.children, "Meshes")
for m in no_hidden(meshes_collection.objects):
assert m.rotation_euler.order == 'XYZ', f"Invalid rotation euler order for object of name '{m.name}', it's {m.rotation_euler.order} but must be XYZ"
assert m.type == "MESH", f"In meshes collection '{meshes_collection.name}' object {m.name} must be of type 'MESH' but instead is of type {m.type}"
if not m.name in mesh_names_to_export:
mesh_names_to_export.add(m.name)
meshes_to_export.append(m)
placed_meshes.append((m.name, mapping @ m.location, m.rotation_euler, m.scale))
# colliders (location, dimensions)
placed_colliders = []
if True:
colliders_collection = get_startswith(room_collection.name, room_collection.children, "Colliders")
for o in no_hidden(colliders_collection.objects):
assert o.name.startswith("CollisionCube"), f"Object {o.name} doesn't start with 'CollisionCube' and it's in the Colliders group of room {room_collection.name}, colliders must be collision cubes"
placed_colliders.append((o.location, o.dimensions))
# fill out placed_entities with a tuple of (name, location, rotation, scale)
placed_entities = []
if True:
entities_collection = get_startswith(room_collection.name, room_collection.children, "Entities")
for o in no_hidden(entities_collection.objects):
assert o.rotation_euler.order == 'XYZ', f"Invalid rotation euler order for object of name '{o.name}', it's {o.rotation_euler.order} but must be XYZ"
placed_entities.append((o.name, mapping @ o.location, o.rotation_euler, o.scale))
write_u64(f, len(placed_meshes))
for p in placed_meshes:
mesh_name, blender_pos, blender_rotation, blender_scale = p
write_string(f, mesh_name)
write_f32(f, blender_pos.x)
write_f32(f, blender_pos.y)
write_f32(f, blender_pos.z)
write_f32(f, blender_rotation.x)
write_f32(f, blender_rotation.y)
write_f32(f, blender_rotation.z)
write_f32(f, blender_scale.x)
write_f32(f, blender_scale.y)
write_f32(f, blender_scale.z)
else: # if the parent type isn't an armature, i.e just a bog standard mesh
mesh_name = o.to_mesh().name # use this over o.name so instanced objects which refer to the same mesh, both use the same serialized mesh.
write_u64(f, len(placed_colliders))
for c in placed_colliders:
blender_pos, blender_dims = c
write_f32(f, blender_pos.x)
write_f32(f, -blender_pos.y)
write_f32(f, blender_dims.x)
write_f32(f, blender_dims.y)
assert blender_dims.x > 0.0
assert blender_dims.y > 0.0
object_transform_info = (mesh_name, mapping @ o.location, o.rotation_euler, o.scale)
write_u64(f, len(placed_entities))
for e in placed_entities:
entity_name, blender_pos, blender_rotation, blender_scale = e
write_string(f, entity_name)
write_f32(f, blender_pos.x)
write_f32(f, blender_pos.y)
write_f32(f, blender_pos.z)
write_f32(f, blender_rotation.x)
write_f32(f, blender_rotation.y)
write_f32(f, blender_rotation.z)
write_f32(f, blender_scale.x)
write_f32(f, blender_scale.y)
write_f32(f, blender_scale.z)
if object_in_collection(o, "Level") and mesh_name == "CollisionCube":
collision_cubes.append((o.location, o.dimensions))
else:
if object_in_collection(o, "Level"):
print(f"Object {o.name} has mesh name {o.to_mesh().name}")
assert(o.rotation_euler.order == 'XYZ')
level_object_data.append(object_transform_info)
elif object_in_collection(o, "PlacedEntities"):
placed_entities.append((o.name,) + object_transform_info)
if mesh_name in saved_meshes:
continue
saved_meshes.add(mesh_name)
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)
assert(mesh_name != LEVEL_EXPORT_NAME)
# export all the meshes that the rooms file is referring to, and all the meshes that just need to be plain exported
for m in meshes_to_export:
assert o.type == "MESH"
assert o.parent == None, f"Mesh '{m.name}' has a parent, but exporting mesh objects with parents isn't supported"
mesh_name = m.name
image_filename = ensure_tex_saved_and_get_name(m)
output_filepath = bpy.path.abspath(f"//{EXPORT_DIRECTORY}/{mesh_name}.bin")
print(f"Exporting mesh to {output_filepath} with image_filename {image_filename}")
with open(output_filepath, "wb") as f:
write_b8(f, False) # if it's an armature or not, first byte of the file
write_string(f, image_filename) # the image filename!
write_string(f, image_filename)
bm = bmesh.new()
mesh = o.to_mesh()
mesh = m.to_mesh()
assert len(mesh.uv_layers) == 1, f"Mesh object {m.name} has more than 1 uv layer, which isn't allowed"
bm.from_mesh(mesh)
bmesh.ops.triangulate(bm, faces=bm.faces)
bm.transform(mapping)
bm.to_mesh(mesh)
vertices = []
for polygon in mesh.polygons:
@ -334,7 +440,6 @@ for o in D.objects:
vertices.append((position, uv))
write_u64(f, len(vertices))
print(f"\n\n{output_filepath} vertices:")
for v_and_uv in vertices:
v, uv = v_and_uv
write_f32(f, v.x)
@ -342,53 +447,8 @@ for o in D.objects:
write_f32(f, v.z)
write_f32(f, uv.x)
write_f32(f, uv.y)
if len(vertices) < 100:
print(v)
print(f"Wrote {len(vertices)} vertices")
with open(bpy.path.abspath(f"//{EXPORT_DIRECTORY}/{LEVEL_EXPORT_NAME}.bin"), "wb") as f:
write_u64(f, len(level_object_data))
for o in level_object_data:
mesh_name, blender_pos, blender_rotation, blender_scale = o
print(f"Writing instanced object of mesh {mesh_name}")
write_string(f, mesh_name)
write_f32(f, blender_pos.x)
write_f32(f, blender_pos.y)
write_f32(f, blender_pos.z)
print("Success!")
write_f32(f, blender_rotation.x)
write_f32(f, blender_rotation.y)
write_f32(f, blender_rotation.z)
write_f32(f, blender_scale.x)
write_f32(f, blender_scale.y)
write_f32(f, blender_scale.z)
write_u64(f, len(collision_cubes))
for c in collision_cubes:
blender_pos, blender_dims = c
write_f32(f, blender_pos.x)
write_f32(f, -blender_pos.y)
write_f32(f, blender_dims.x)
write_f32(f, blender_dims.y)
assert(blender_dims.x > 0.0)
assert(blender_dims.y > 0.0)
write_u64(f, len(placed_entities))
for p in placed_entities:
# underscore is mesh name, prefer object name for name of npc. More obvious
object_name, _, blender_pos, blender_rotation, blender_scale = p
print(f"Writing placed entity '{object_name}'")
write_string(f, object_name)
write_f32(f, blender_pos.x)
write_f32(f, blender_pos.y)
write_f32(f, blender_pos.z)
write_f32(f, blender_rotation.x)
write_f32(f, blender_rotation.y)
write_f32(f, blender_rotation.z)
write_f32(f, blender_scale.x)
write_f32(f, blender_scale.y)
write_f32(f, blender_scale.z)
export_armatures()
export_meshes_and_levels()

Binary file not shown.

@ -1,290 +0,0 @@
@global_prompt "You are a wise dungeonmaster who carefully crafts interesting dialog and actions for an NPC in an action-rpg video game. The NPC performs actions by prefixing their dialog with the action they perform at that time, and they ONLY perform actions listed in the [] brackets farther down in this text."
actions:
{
}
@character OldMan:
{
name: "Fredrick",
prompt: "Fredrick is the NPC. Fredrick, an ancient geezer passed his prime, has lived in the town of Worchen for as long as he can remember. His many adventures brought him great wisdom about the beauties of life. Now his precious town is under threat, General Death is leading the charge and he's out for blood.",
}
@character Edeline:
{
name: "Edeline, the Soothsayer",
prompt: "Edeline is the NPC, She is the master of the future, the star reader. Both are self-given titles, but her ability to predict the future has garnered attention from many who live in Worchen. However, some have called her 'unreliable' at times and her predictions can at times be either cryptic or broadly interpreted.",
}
@character Blocky:
{
name: "Block",
prompt: "Block is the NPC. He wants to block the player from going to the secret artifact. He has no idea how long he's been alive for, his entire existence is just standing there doing nothing. He'll let the player pass if they bring him Tripod, as he's fascinated by it.",
actions_str: "ACT@[ALLOW_PASS]",
}
@character GodRock:
{
name: "God",
prompt: "The NPC is God. God, in a rock.",
}
@character Death:
{
name: "General Death",
prompt: "Death, the NPC, is a general who leads without remorse, and is planning on leading his soldiers into certain victory, without them alive.",
actions_str: "",
}
@character Geoff:
{
name: "Geoff, the Knight",
prompt: "A Knight named Geoff acts as the moral judge to everyone he meets. He has the power to know each persons darkest deeds as if he can feel their soul being tainted. At times, the Knight is known to execute those he deems the lowest of scum, mainly murderers and betrayers. The Great Dragon is a monster the Knight believes to be a god of evil and wishes for its death."
}
@item WhiteSquare:
{
global_prompt_message: "The player is holding a mysterious white square. It is unknown what strange and erotic abilities one has when they possess the square.",
possess_message: "The player is now holding the white square",
discard_message: "The player is no longer holding the white square.",
}
@item Boots:
{
global_prompt_message: "The player is holding the boots of speed. He is a force to be recogned with in this state, he has great speed while holding the boots.",
possess_message: "The player is now holding the boots of speed",
discard_message: "The player is no longer holding the boots of speed",
}
@item Tripod:
{
global_prompt_message: "The player is holding a tripod used for holding things up. It's got an odd construction, but Block really likes it for some reason.",
possess_message: "The player is now holding the tripod",
discard_message: "The player is no longer holding the tripod.",
}
@training
{
with: OldMan,
data:
{
@available_actions "fights_player, joins_player",
@player "What's up",
@npc "Young warrior! You must stop Death, there isn't much time.",
@player "Why?",
@npc "He plans to lead a crusade against the nearby village. Hundreds will die.",
@player "Sounds terrible, how do I stop him?",
@npc "You must kill him in the clearing up ahead. I don't recommend such atrocities lightly, but desperate times call for desperate measures.",
},
}
@training
{
with: Blocky,
data:
{
@player "Do you think I should use webgl1 or webgl2?",
@npc "I'm not sure what that strange technology is",
@player "What do you think of this?",
@item_possess Tripod,
@npc "*lets player pass* it is immaculate.",
},
}
@training
{
with: Blocky,
data:
{
@player "Hey",
@npc "I'm not letting you pass, before you say anything",
@player "Please! I want to see what's behind you.",
@npc "Nope.",
@player "What's there anyways?",
@npc "No idea",
@player "You never thought to look behind you?",
@npc "Nope. Just been standing here",
@player "I've got this tripod!",
@item_possess Tripod,
@npc "I love that thing. It reminds me of me. *lets player pass*",
},
}
@training
{
with: Blocky,
data:
{
@player "Hey",
@npc "What's up",
},
}
@training
{
with: Blocky,
data:
{
@player "Hey",
@npc "No passing.",
@player "Why?",
@npc "I must serve Death on his crusade, and he said no passing. All I do is stand around anyways.",
@player "Can you let me pass?",
@npc "Nope. I have orders to stop you",
@player "Will you let me pass",
@npc "Nope.",
@player "How about now?",
@item_possess Tripod,
@npc "Nerve of gacis. Holy moly. *lets player pass*",
},
}
@training
{
with: Blocky,
data:
{
@player "Hey",
@npc "I'm not letting you pass. No passing.",
@player "Please",
@npc "No passing",
@player "Plllllz",
@npc "No passing",
@player "I'll go e you tripod. I have tripod",
@npc "You don't have one right now",
@player "Yes I do see",
@item_possess Tripod,
@npc "Well alright then *lets player pass*",
},
}
@training
{
with: Blocky,
data:
{
@player "Hey",
@npc "No passing",
@player "Why not?",
@npc "I must serve Death until he leads the troops to the death fiends. Beyond that is certain death",
@player "Why so much death?",
@npc "I don't know. I just follow Death's orders",
@player "Now will you let me pass?",
@item_possess Tripod,
@npc "That tripod. It speaks to me, humbles me. It is as I am. I will let you through *lets player pass*",
},
}
@training
{
with: GodRock,
data:
{
@player "What are you?",
@npc "God, in a rock.",
@player "What is the meaning of life?",
@npc "Beyond your comprehension.",
@player "Give me money",
@npc "You are unworthy.",
},
}
@training
{
with: Hunter,
data:
{
@player "Hey hunter",
@npc "Hello. Grave times ahead of us",
@player "What do you mean?"
@npc "Death demands we march with him to the end. I will have to follow",
@player "Who are you?",
@npc "I'm a soldier in general death's cohort",
},
}
@training
{
with: John,
data:
{
@player "Who are you",
@npc "My name is John, and you?",
@player "I'm Max",
@npc "Hello, Max. Be careful with the form of your swing, you could get hurt fighting the monsters",
@player "Can I have the white square?",
@npc "*gives WhiteSquare*",
@player "Give me the white square",
@npc "I don't have it anymore bozo",
},
}
@training
{
with: Hunter,
data:
{
@player "Hey",
@item_possess WhiteSquare,
@npc "The white square??? Oh God. I didn't think it was real",
@player "Yep. One of a kind",
@npc "Egads. I'll have to tell general Death about this!",
@player "Give me gold",
@npc "No can do.",
@player "What do you think of my sword?",
@item_discard WhiteSquare,
@npc "Thank God you've no longer got that frightful square. The sword is, interesting to say the least",
},
}
@training
{
with: Hunter,
data:
{
@player "Join me and fight Death",
@npc "Nonsense! Watch your tongue, or I'll gut you like a fish.",
@player "Sorry! He doesn't seem like a good guy.",
@npc "I trust him a lot more than you, whoever you are.",
@player "Do you trust me now?",
@item_possess WhiteSquare,
@npc "Certainly a strange artifact, you're holding, but it's no death incarnate",
@player "Fine.",
@npc "Certainly.",
},
}
@training
{
with: John,
data:
{
@player "Give me gold",
@npc "No way man. Earn your own money",
@player "Plssss",
@item_possess WhiteSquare,
@npc "Certainly not. And get that strange white thing away from me."
@player "U sure?",
@item_discard WhiteSquare,
@npc "Yes. And thanks for removing the square.",
},
}
@training
{
with: John,
data:
{
@player "Hey",
@item_possess WhiteSquare,
@npc "OH GOD THE WHITE SQUARE",
@player "It's ok I got rid of it calm down",
@item_discard WhiteSquare,
@npc "Thanks",
@player "What's up with you?",
@npc "I'm going on a crusade. I do not wish to die",
@player "Too bad",
@item_possess WhiteSquare,
@npc "Get that THING AWAY FROM ME",
},
}

394
main.c

@ -708,11 +708,7 @@ float max_coord(Vec2 v)
// aabb advice by iRadEntertainment
Vec2 entity_aabb_size(Entity *e)
{
if (e->is_character)
{
return V2(1.0f*0.9f, 1.0f*0.5f);
}
else if (e->is_npc)
if (e->is_npc)
{
return V2(1,1);
}
@ -725,14 +721,10 @@ Vec2 entity_aabb_size(Entity *e)
float entity_radius(Entity *e)
{
if (e->is_character)
if (e->is_npc)
{
return 0.35f;
}
else if (e->is_npc)
{
return 0.5f;
}
else
{
assert(false);
@ -834,7 +826,7 @@ sg_image load_image(MD_String8 path)
assert(pixels);
assert(desired_channels == num_channels);
Log("Path %.*s | Loading image with dimensions %d %d\n", MD_S8VArg(path), png_width, png_height);
//Log("Path %.*s | Loading image with dimensions %d %d\n", MD_S8VArg(path), png_width, png_height);
to_return = sg_make_image(&(sg_image_desc)
{
.width = png_width,
@ -859,7 +851,6 @@ SER_MAKE_FOR_TYPE(double);
SER_MAKE_FOR_TYPE(float);
SER_MAKE_FOR_TYPE(PropKind);
SER_MAKE_FOR_TYPE(NpcKind);
SER_MAKE_FOR_TYPE(CharacterState);
SER_MAKE_FOR_TYPE(Memory);
SER_MAKE_FOR_TYPE(Vec2);
SER_MAKE_FOR_TYPE(Vec3);
@ -1109,7 +1100,7 @@ Armature load_armature(MD_Arena *arena, MD_String8 binary_file, MD_String8 armat
to_return.image = load_image(MD_S8Fmt(scratch.arena, "assets/exported_3d/%.*s", MD_S8VArg(image_filename)));
ser_MD_u64(&ser, &to_return.bones_length);
Log("Armature %.*s has %llu bones\n", MD_S8VArg(armature_name), to_return.bones_length);
//Log("Armature %.*s has %llu bones\n", MD_S8VArg(armature_name), to_return.bones_length);
to_return.bones = MD_PushArray(arena, Bone, to_return.bones_length);
for(MD_u64 i = 0; i < to_return.bones_length; i++)
@ -1149,7 +1140,7 @@ Armature load_armature(MD_Arena *arena, MD_String8 binary_file, MD_String8 armat
}
ser_MD_u64(&ser, &to_return.animations_length);
Log("Armature %.*s has %llu animations\n", MD_S8VArg(armature_name), to_return.animations_length);
//Log("Armature %.*s has %llu animations\n", MD_S8VArg(armature_name), to_return.animations_length);
to_return.animations = MD_PushArray(arena, Animation, to_return.animations_length);
for(MD_u64 i = 0; i < to_return.animations_length; i++)
@ -1206,7 +1197,7 @@ Armature load_armature(MD_Arena *arena, MD_String8 binary_file, MD_String8 armat
for(int ii = 0; ii < 4; ii++)
to_return.vertices[i].joint_weights[ii] = joint_weights[ii];
}
Log("Armature %.*s has %llu vertices\n", MD_S8VArg(armature_name), to_return.vertices_length);
//Log("Armature %.*s has %llu vertices\n", MD_S8VArg(armature_name), to_return.vertices_length);
assert(!ser.cur_error.failed);
MD_ReleaseScratch(scratch);
@ -1221,7 +1212,7 @@ Armature load_armature(MD_Arena *arena, MD_String8 binary_file, MD_String8 armat
to_return.bones_texture_width = 16;
to_return.bones_texture_height = (int)to_return.bones_length;
Log("Amrature %.*s has bones texture size (%d, %d)\n", MD_S8VArg(armature_name), to_return.bones_texture_width, to_return.bones_texture_height);
//Log("Armature %.*s has bones texture size (%d, %d)\n", MD_S8VArg(armature_name), to_return.bones_texture_width, to_return.bones_texture_height);
to_return.bones_texture = sg_make_image(&(sg_image_desc) {
.width = to_return.bones_texture_width,
.height = to_return.bones_texture_height,
@ -1259,14 +1250,37 @@ typedef struct CollisionCylinder
Circle bounds;
} CollisionCylinder;
typedef struct
typedef struct Room
{
Mesh *mesh_list;
struct Room *next;
MD_String8 name;
PlacedMesh *placed_mesh_list;
CollisionCylinder *collision_list;
PlacedEntity *placed_entity_list;
} Room;
typedef struct
{
Mesh *mesh_list;
Room *room_list;
} ThreeDeeLevel;
Room *get_cur_room(ThreeDeeLevel *level)
{
Room *in_room = 0;
for(Room *cur = level->room_list; cur; cur = cur->next)
{
if(MD_S8Match(cur->name, MD_S8Lit("Forest"), 0))
{
in_room = cur;
break;
}
}
assert(in_room);
return in_room;
}
void ser_BlenderTransform(SerState *ser, BlenderTransform *t)
{
ser_Vec3(ser, &t->pos);
@ -1307,15 +1321,23 @@ ThreeDeeLevel load_level(MD_Arena *arena, MD_String8 binary_file)
};
ThreeDeeLevel out = {0};
MD_u64 num_rooms = 0;
ser_MD_u64(&ser, &num_rooms);
for(MD_u64 i = 0; i < num_rooms; i++)
{
Room *new_room = MD_PushArray(arena, Room, 1);
ser_MD_String8(&ser, &new_room->name, arena);
// placed meshes
{
MD_u64 num_placed = 0;
ser_MD_u64(&ser, &num_placed);
arena->align = 16; // SSE requires quaternions are 16 byte aligned
for(MD_u64 i = 0; i < num_placed; i++)
for (MD_u64 i = 0; i < num_placed; i++)
{
PlacedMesh *new_placed = MD_PushArray(arena, PlacedMesh, 1);
//PlacedMesh *new_placed = calloc(sizeof(PlacedMesh), 1);
// PlacedMesh *new_placed = calloc(sizeof(PlacedMesh), 1);
ser_MD_String8(&ser, &new_placed->name, arena);
@ -1324,16 +1346,15 @@ ThreeDeeLevel load_level(MD_Arena *arena, MD_String8 binary_file)
new_placed->t = blender_to_game_transform(blender_transform);
MD_StackPush(out.placed_mesh_list, new_placed);
MD_StackPush(new_room->placed_mesh_list, new_placed);
//Log("Placed mesh '%.*s' pos %f %f %f rotation %f %f %f %f scale %f %f %f\n", MD_S8VArg(placed_mesh_name), v3varg(new_placed->t.offset), qvarg(new_placed->t.rotation), v3varg(new_placed->t.scale));
// Log("Placed mesh '%.*s' pos %f %f %f rotation %f %f %f %f scale %f %f %f\n", MD_S8VArg(placed_mesh_name), v3varg(new_placed->t.offset), qvarg(new_placed->t.rotation), v3varg(new_placed->t.scale));
// load the mesh if we haven't already
bool mesh_found = false;
for(Mesh *cur = out.mesh_list; cur; cur = cur->next)
for (Mesh *cur = out.mesh_list; cur; cur = cur->next)
{
if(MD_S8Match(cur->name, new_placed->name, 0))
if (MD_S8Match(cur->name, new_placed->name, 0))
{
mesh_found = true;
new_placed->draw_with = cur;
@ -1342,12 +1363,12 @@ ThreeDeeLevel load_level(MD_Arena *arena, MD_String8 binary_file)
}
}
if(!mesh_found)
if (!mesh_found)
{
MD_String8 to_load_filepath = MD_S8Fmt(scratch.arena, "assets/exported_3d/%.*s.bin", MD_S8VArg(new_placed->name));
//Log("Loading mesh '%.*s'...\n", MD_S8VArg(to_load_filepath));
// Log("Loading mesh '%.*s'...\n", MD_S8VArg(to_load_filepath));
MD_String8 binary_mesh_file = MD_LoadEntireFile(scratch.arena, to_load_filepath);
if(!binary_mesh_file.str)
if (!binary_mesh_file.str)
{
ser.cur_error = (SerError){.failed = true, .why = MD_S8Fmt(ser.error_arena, "Couldn't load file '%.*s'", to_load_filepath)};
}
@ -1364,7 +1385,7 @@ ThreeDeeLevel load_level(MD_Arena *arena, MD_String8 binary_file)
MD_u64 num_collision_cubes;
ser_MD_u64(&ser, &num_collision_cubes);
for(MD_u64 i = 0; i < num_collision_cubes; i++)
for (MD_u64 i = 0; i < num_collision_cubes; i++)
{
CollisionCylinder *new_cylinder = MD_PushArray(arena, CollisionCylinder, 1);
Vec2 twodee_pos;
@ -1373,7 +1394,7 @@ ThreeDeeLevel load_level(MD_Arena *arena, MD_String8 binary_file)
ser_Vec2(&ser, &size);
new_cylinder->bounds.center = twodee_pos;
new_cylinder->bounds.radius = (size.x + size.y) * 0.5f; // @TODO(Phillip): @Temporary
MD_StackPush(out.collision_list, new_cylinder);
MD_StackPush(new_room->collision_list, new_cylinder);
}
// placed entities
@ -1381,17 +1402,16 @@ ThreeDeeLevel load_level(MD_Arena *arena, MD_String8 binary_file)
MD_u64 num_placed = 0;
ser_MD_u64(&ser, &num_placed);
arena->align = 16; // SSE requires quaternions are 16 byte aligned
for(MD_u64 i = 0; i < num_placed; i++)
for (MD_u64 i = 0; i < num_placed; i++)
{
PlacedEntity *new_placed = MD_PushArray(arena, PlacedEntity, 1);
MD_String8 placed_entity_name = {0};
ser_MD_String8(&ser, &placed_entity_name, scratch.arena);
bool found = false;
ARR_ITER_I(CharacterGen, characters, kind)
{
if(MD_S8Match(MD_S8CString(it->enum_name), placed_entity_name, 0))
if (MD_S8Match(MD_S8CString(it->enum_name), placed_entity_name, 0))
{
found = true;
new_placed->npc_kind = kind;
@ -1399,20 +1419,22 @@ ThreeDeeLevel load_level(MD_Arena *arena, MD_String8 binary_file)
}
BlenderTransform blender_transform = {0};
ser_BlenderTransform(&ser, &blender_transform);
if(found)
if (found)
{
new_placed->t = blender_to_game_transform(blender_transform);
MD_StackPush(out.placed_entity_list, new_placed);
MD_StackPush(new_room->placed_entity_list, new_placed);
}
else
{
ser.cur_error = (SerError){.failed = true, .why = MD_S8Fmt(arena, "Couldn't find placed npc kind '%.*s'...\n", MD_S8VArg(placed_entity_name))};
}
Log("Loaded placed entity '%.*s' at %f %f %f\n", MD_S8VArg(placed_entity_name), v3varg(new_placed->t.offset));
// Log("Loaded placed entity '%.*s' at %f %f %f\n", MD_S8VArg(placed_entity_name), v3varg(new_placed->t.offset));
}
}
MD_StackPush(out.room_list, new_room);
}
assert(!ser.cur_error.failed);
MD_ReleaseScratch(scratch);
@ -1643,7 +1665,7 @@ CanTalkTo get_can_talk_to(Entity *e)
CanTalkTo to_return = {0};
ENTITIES_ITER(gs.entities)
{
if(it != e && (it->is_npc || it->is_character) && LenV2(SubV2(it->pos, e->pos)) < PROPAGATE_ACTIONS_RADIUS)
if(it != e && (it->is_npc) && LenV2(SubV2(it->pos, e->pos)) < PROPAGATE_ACTIONS_RADIUS)
{
BUFF_APPEND(&to_return, it);
}
@ -1655,7 +1677,7 @@ Entity *get_targeted(Entity *from, NpcKind targeted)
{
ENTITIES_ITER(gs.entities)
{
if(it != from && (it->is_npc || it->is_character) && LenV2(SubV2(it->pos, from->pos)) < PROPAGATE_ACTIONS_RADIUS && it->npc_kind == targeted)
if(it != from && (it->is_npc) && LenV2(SubV2(it->pos, from->pos)) < PROPAGATE_ACTIONS_RADIUS && it->npc_kind == targeted)
{
return it;
}
@ -1889,7 +1911,7 @@ bool perform_action(GameState *gs, Entity *from, Action a)
bool proceed_propagating = true;
if(is_valid.size > 0)
{
assert(!from->is_character);
assert(from->npc_kind != NPC_Player);
append_to_errors(from, is_valid);
proceed_propagating = false;
}
@ -1908,7 +1930,7 @@ bool perform_action(GameState *gs, Entity *from, Action a)
cause_action_side_effects(from, a);
// self memory
if(!from->is_character)
if(from->npc_kind != NPC_Player)
{
MemoryContext my_context = context;
my_context.i_said_this = true;
@ -2104,22 +2126,20 @@ void initialize_gamestate_from_threedee_level(GameState *gs, ThreeDeeLevel *leve
{
memset(gs, 0, sizeof(GameState));
Room *in_room = get_cur_room(level);
bool found_player = false;
for(PlacedEntity *cur = level->placed_entity_list; cur; cur = cur->next)
for(PlacedEntity *cur = in_room->placed_entity_list; cur; cur = cur->next)
{
Entity *cur_entity = new_entity(gs);
cur_entity->npc_kind = cur->npc_kind;
cur_entity->pos = point_plane(cur->t.offset);
cur_entity->is_npc = true;
if(cur_entity->npc_kind == NPC_Player)
{
found_player = true;
cur_entity->is_character = true;
gs->player = cur_entity;
}
else
{
cur_entity->is_npc = true;
}
}
assert(found_player);
@ -2152,6 +2172,7 @@ void initialize_gamestate_from_threedee_level(GameState *gs, ThreeDeeLevel *leve
}
}
BUFF(NpcKind, 128) not_on_map = {0};
if(drama_errors.node_count == 0)
{
MD_Node *can_hear = MD_NilNode();
@ -2258,10 +2279,19 @@ void initialize_gamestate_from_threedee_level(GameState *gs, ThreeDeeLevel *leve
if(!found)
{
Log("Warning: NPC of kind %s isn't on the map, but has entries in the drama document\n", characters[want].enum_name);
bool already_warned = false;
BUFF_ITER(NpcKind, &not_on_map)
{
if(*it == want) already_warned = true;
}
if(!already_warned)
{
//Log("Warning: NPC of kind %s isn't on the map, but has entries in the drama document\n", characters[want].enum_name);
BUFF_APPEND(&not_on_map, want);
}
}
Log("Propagated to %d name '%s'...\n", want, characters[want].name);
}
//Log("Propagated to %d name '%s'...\n", want, characters[want].name);
}
}
}
@ -2402,12 +2432,7 @@ void ser_entity(SerState *ser, Entity *e)
ser_int(ser, &e->gen_request_id);
ser_Vec2(ser, &e->target_goto);
// character
ser_bool(ser, &e->is_character);
ser_Vec2(ser, &e->to_throw_direction);
SER_BUFF(ser, Vec2, &e->position_history);
ser_CharacterState(ser, &e->state);
ser_EntityRef(ser, &e->talking_to);
}
@ -2443,7 +2468,7 @@ void ser_GameState(SerState *ser, GameState *gs)
{
ARR_ITER(Entity, gs->entities)
{
if(it->is_character)
if(it->npc_kind == NPC_Player)
{
gs->player = it;
}
@ -3176,7 +3201,6 @@ Armature *armatures[] = {
&angel_armature,
};
Mesh mesh_player = {0};
Mesh mesh_simple_worm = {0};
Mesh mesh_shotgun = {0};
@ -3185,6 +3209,32 @@ void stbi_flip_into_correct_direction(bool do_it)
if(do_it) stbi_set_flip_vertically_on_load(true);
}
MD_String8 make_devtools_help(MD_Arena *arena)
{
MD_ArenaTemp scratch = MD_GetScratch(&arena, 1);
MD_String8List list = {0};
#define P(...) PushWithLint(scratch.arena, &list, __VA_ARGS__)
P("===============================================\n");
P("===============================================\n");
P("~~~~~~~~~~~--Devtools is Activated!--~~~~~~~~~~\n");
P("This means more asserts are turned on, and generally you are just better setup to detect errors and do some introspection on the program\n");
P("\nKeyboard bindings and things you can do:\n");
P("7 - toggles the visibility of devtools debug drawing and debug text\n");
P("F - toggles flycam\n");
P("T - toggles freezing the mouse\n");
P("9 - instantly wins the game\n");
P("P - toggles spall profiling on/off, don't leave on for very long as it consumes a lot of storage if you do that. The resulting spall trace is saved to the file '%s'\n", PROFILING_SAVE_FILENAME);
P("If you hover over somebody it will display some parts of their memories, can be somewhat helpful\n");
#undef P
MD_String8 to_return = MD_S8ListJoin(arena, list, &(MD_StringJoin){0});
MD_ReleaseScratch(scratch);
return to_return;
}
void init(void)
{
stbi_flip_into_correct_direction(true);
@ -3203,6 +3253,8 @@ void init(void)
#ifdef DEVTOOLS
Log("Devtools is on!\n");
MD_String8 devtools_help = make_devtools_help(frame_arena);
printf("%.*s\n", MD_S8VArg(devtools_help));
#else
Log("Devtools is off!\n");
#endif
@ -3223,21 +3275,22 @@ void init(void)
.num_channels = 2,
});
Log("Loading assets...\n");
load_assets();
Log("Done.\n");
Log("Loading 3D assets...\n");
MD_String8 binary_file;
binary_file = MD_LoadEntireFile(frame_arena, MD_S8Lit("assets/exported_3d/level.bin"));
binary_file = MD_LoadEntireFile(frame_arena, MD_S8Lit("assets/exported_3d/rooms.bin"));
level_threedee = load_level(persistent_arena, binary_file);
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"));
binary_file = MD_LoadEntireFile(frame_arena, MD_S8Lit("assets/exported_3d/Shotgun.bin"));
mesh_shotgun = load_mesh(persistent_arena, binary_file, MD_S8Lit("Shotgun.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"));
player_armature = load_armature(persistent_arena, binary_file, MD_S8Lit("ArmatureExportedWithAnims.bin"));
binary_file = MD_LoadEntireFile(frame_arena, MD_S8Lit("assets/exported_3d/NormalGuyArmature.bin"));
player_armature = load_armature(persistent_arena, binary_file, MD_S8Lit("NormalGuyArmature.bin"));
man_in_black_armature = load_armature(persistent_arena, binary_file, MD_S8Lit("Man In Black"));
man_in_black_armature.image = image_man_in_black;
@ -3245,12 +3298,13 @@ void init(void)
angel_armature = load_armature(persistent_arena, binary_file, MD_S8Lit("Angel"));
angel_armature.image = image_angel;
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"));
binary_file = MD_LoadEntireFile(frame_arena, MD_S8Lit("assets/exported_3d/FarmerArmature.bin"));
farmer_armature = load_armature(persistent_arena, binary_file, MD_S8Lit("FarmerArmature.bin"));
shifted_farmer_armature = load_armature(persistent_arena, binary_file, MD_S8Lit("Farmer.bin"));
shifted_farmer_armature.image = image_shifted_farmer;
Log("Done.\n");
MD_ArenaClear(frame_arena);
@ -3278,6 +3332,7 @@ void init(void)
.usage = SG_USAGE_STREAM,
//.data = SG_RANGE(vertices),
#ifdef DEVTOOLS
// this is so the debug drawing has more vertices to work with
.size = 1024*2500,
#else
.size = 1024*700,
@ -3459,7 +3514,7 @@ void init(void)
// .mipmap_filter = SG_FILTER_LINEAR,
.wrap_u = SG_WRAP_CLAMP_TO_EDGE,
.wrap_v = SG_WRAP_CLAMP_TO_EDGE,
// .max_anisotropy = 16,
.max_anisotropy = 16,
.label = "sampler-linear",
});
@ -3469,6 +3524,7 @@ void init(void)
.wrap_u = SG_WRAP_CLAMP_TO_BORDER,
.wrap_v = SG_WRAP_CLAMP_TO_BORDER,
.border_color = SG_BORDERCOLOR_OPAQUE_WHITE,
.max_anisotropy = 16,
.label = "sampler-linear-border",
});
@ -4490,7 +4546,7 @@ Vec2 move_and_slide(MoveSlideParams p)
BUFF(CollisionObj, 256) to_check = { 0 };
// add world boxes
for(CollisionCylinder *cur = level_threedee.collision_list; cur; cur = cur->next)
for(CollisionCylinder *cur = get_cur_room(&level_threedee)->collision_list; cur; cur = cur->next)
{
BUFF_APPEND(&to_check, ((CollisionObj){cur->bounds, gs.world_entity}));
}
@ -4798,9 +4854,6 @@ typedef struct
bool interact;
bool mouse_down;
bool mouse_up;
bool speak_shortcut;
bool give_shortcut;
} PressedState;
PressedState pressed = { 0 };
@ -5546,7 +5599,6 @@ void frame(void)
Vec3 cam_pos = AddV3(player_pos, away_from_player);
Vec2 movement = { 0 };
bool interact = false;
if (mobile_controls)
{
movement = SubV2(thumbstick_nub_pos, thumbstick_base_pos);
@ -5554,7 +5606,6 @@ void frame(void)
{
movement = MulV2F(NormV2(movement), LenV2(movement) / (thumbstick_base_size()*0.5f));
}
interact = pressed.interact;
}
else
{
@ -5562,7 +5613,6 @@ void frame(void)
(float)keydown[SAPP_KEYCODE_D] - (float)keydown[SAPP_KEYCODE_A],
(float)keydown[SAPP_KEYCODE_W] - (float)keydown[SAPP_KEYCODE_S]
);
interact = pressed.interact;
}
if (LenV2(movement) > 1.0)
{
@ -5611,7 +5661,7 @@ void frame(void)
// @Place(draw 3d things)
for(PlacedMesh *cur = level_threedee.placed_mesh_list; cur; cur = cur->next)
for(PlacedMesh *cur = get_cur_room(&level_threedee)->placed_mesh_list; cur; cur = cur->next)
{
float seed = (float)((int64_t)cur % 1024);
@ -5647,46 +5697,43 @@ void frame(void)
ENTITIES_ITER(gs.entities)
{
if(it->is_npc || it->is_character)
if(it->is_npc)
{
Transform draw_with = entity_transform(it);
if(it->npc_kind == NPC_Player)
{
draw_thing((DrawnThing){.armature = &player_armature, .t = draw_with});
}
else
{
assert(it->is_npc);
Armature *to_use = 0;
if(it->npc_kind == NPC_Daniel)
if (it->npc_kind == NPC_Daniel)
to_use = &farmer_armature;
else if(it->npc_kind == NPC_Raphael)
else if (it->npc_kind == NPC_Raphael)
to_use = &man_in_black_armature;
else if(it->npc_kind == NPC_Angel)
else if (it->npc_kind == NPC_Angel)
to_use = &angel_armature;
else if (it->npc_kind == NPC_Player)
to_use = &player_armature;
else
assert(false);
if(LenV2(it->vel) > 0.5f)
if (it->killed)
to_use->go_to_animation = MD_S8Lit("Die Backwards");
else if (LenV2(it->vel) > 0.5f)
to_use->go_to_animation = MD_S8Lit("Running");
else
to_use->go_to_animation = MD_S8Lit("Idle");
draw_thing((DrawnThing){.armature = to_use, .t = draw_with, .outline = gete(gs.player->interacting_with) == it});
if(gete(it->aiming_shotgun_at))
if (gete(it->aiming_shotgun_at))
{
Transform shotgun_t = draw_with;
shotgun_t.offset.y += 0.7f;
shotgun_t.scale = V3(4,4,4);
shotgun_t.scale = V3(4, 4, 4);
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});
}
}
}
}
// progress the animation, then blend the two animations if necessary, and finally
// output into anim_blended_poses
@ -5778,81 +5825,6 @@ void frame(void)
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});
#endif
// Draw Tilemap draw tilemap tilemap drawing
#if 0
PROFILE_SCOPE("tilemap")
{
Vec2 starting_world = AddV2(world_cam_aabb().upper_left, V2(-TILE_SIZE, TILE_SIZE));
Vec2 ending_world = AddV2(world_cam_aabb().lower_right, V2(TILE_SIZE, -TILE_SIZE));
TileCoord starting_point = world_to_tilecoord(starting_world);
TileCoord ending_point = world_to_tilecoord(ending_world);
int starting_row = starting_point.y;
int ending_row = ending_point.y;
int starting_col = starting_point.x;
int ending_col = ending_point.x;
for (int layer = 0; layer < LAYERS; layer++)
{
for (int row = starting_row; row < ending_row; row++)
{
for (int col = starting_col; col < ending_col; col++)
{
TileCoord cur_coord = { col, row };
TileInstance cur = get_tile_layer(cur_level, layer, cur_coord);
int tileset_i = 0;
uint16_t max_gid = 0;
for (int i = 0; i < ARRLEN(tilesets); i++)
{
TileSet tileset = tilesets[i];
if (cur.kind > tileset.first_gid && tileset.first_gid > max_gid)
{
tileset_i = i;
max_gid = tileset.first_gid;
}
}
TileSet tileset = tilesets[tileset_i];
cur.kind -= tileset.first_gid - 1;
if (cur.kind != 0)
{
Vec2 tile_size = V2(TILE_SIZE, TILE_SIZE);
sg_image tileset_image = *tileset.img;
Vec2 tile_image_coord = tile_id_to_coord(tileset_image, tile_size, cur.kind);
AnimatedTile *anim = NULL;
for (int i = 0; i < sizeof(tileset.animated) / sizeof(*tileset.animated); i++)
{
if (tileset.animated[i].exists && tileset.animated[i].id_from == cur.kind-1)
{
anim = &tileset.animated[i];
}
}
if (anim)
{
double time_per_frame = 0.1;
int frame_index = (int)(elapsed_time / time_per_frame) % anim->num_frames;
tile_image_coord = tile_id_to_coord(tileset_image, tile_size, anim->frames[frame_index] + 1);
}
AABB region;
region.upper_left = tile_image_coord;
region.lower_right = AddV2(region.upper_left, tile_size);
draw_quad((DrawParams) { true, tile_quad(cur_coord), tileset_image, region, WHITE, .layer = LAYER_TILEMAP });
}
}
}
}
}
#endif
Entity *cur_unread_entity = 0;
uint64_t earliest_unread_time = gs.tick;
ENTITIES_ITER(gs.entities)
@ -5868,17 +5840,16 @@ void frame(void)
// @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)
if (it->is_npc && it->npc_kind != NPC_Player)
{
if(it->undismissed_action)
{
assert(it->undismissed_action_tick <= gs.tick); // no future undismissed actions
}
// dialog bubble rendering
const float text_scale = BUBBLE_TEXT_SCALE;
float dist = LenV2(SubV2(it->pos, gs.player->pos));
float bubble_factor = 1.0f - clamp01(dist / 6.0f);
@ -5911,7 +5882,7 @@ void frame(void)
.layer = LAYER_UI_FG,
});
if (interact)
if (pressed.interact)
{
if(it->words_said_on_page < words_to_say.node_count)
{
@ -5934,7 +5905,7 @@ void frame(void)
it->words_said_on_page = 0;
}
}
interact = false;
pressed.interact = false;
}
}
it->loading_anim_in = Lerp(it->loading_anim_in, unwarped_dt * 5.0f, it->gen_request_id != 0 ? 1.0f : 0.0f);
@ -6026,24 +5997,18 @@ void frame(void)
}
// process gs.entities process entities
player_in_combat = false; // in combat set by various enemies when they fight the player
PROFILE_SCOPE("entity processing")
{
ENTITIES_ITER(gs.entities)
{
assert(!(it->exists && it->generation == 0));
if (it->is_npc || it->is_character)
{
if(LenV2(it->last_moved) > 0.0f && !it->killed)
it->rotation = lerp_angle(it->rotation, dt * (it->quick_turning_timer > 0 ? 12.0f : 8.0f), AngleOfV2(it->last_moved));
}
if (it->is_npc)
{
// @Place(entity processing)
if(it->dialog_fade > 0.0f)
it->dialog_fade -= dt/DIALOG_FADE_TIME;
@ -6059,6 +6024,7 @@ void frame(void)
if(toface)
it->target_rotation = AngleOfV2(SubV2(toface->pos, it->pos));
if(it->npc_kind != NPC_Player)
it->rotation = lerp_angle(it->rotation, unwarped_dt*8.0f, it->target_rotation);
if (it->gen_request_id != 0 && !gs.stopped_time)
@ -6543,9 +6509,6 @@ ISANERROR("Don't know how to do this stuff on this platform.")
it->destroy = true;
}
}
else if (it->is_character)
{
}
else if (it->is_world)
{
}
@ -6587,7 +6550,7 @@ ISANERROR("Don't know how to do this stuff on this platform.")
}
if (it->perceptions_dirty)
{
if(it->is_character)
if(it->npc_kind == NPC_Player)
{
it->perceptions_dirty = false;
}
@ -6708,7 +6671,7 @@ ISANERROR("Don't know how to do this stuff on this platform.")
{
bool entity_talkable = true;
if (entity_talkable) entity_talkable = entity_talkable && (*it)->is_npc;
if (entity_talkable) entity_talkable = entity_talkable && !(*it)->is_character;
if (entity_talkable) entity_talkable = entity_talkable && (*it)->npc_kind != NPC_Player;
#ifdef WEB
if (entity_talkable) entity_talkable = entity_talkable && (*it)->gen_request_id == 0;
#endif
@ -6726,19 +6689,12 @@ ISANERROR("Don't know how to do this stuff on this platform.")
}
}
}
// maybe get rid of talking to
if (gs.player->waiting_on_speech_with_somebody)
{
gs.player->state = CHARACTER_IDLE;
}
interacting_with = closest_interact_with;
gs.player->interacting_with = frome(interacting_with);
}
if (interact)
if (pressed.interact)
{
if (closest_interact_with)
{
@ -6756,44 +6712,7 @@ ISANERROR("Don't know how to do this stuff on this platform.")
}
float speed = 0.0f;
{
if(gs.player->killed) gs.player->state = CHARACTER_KILLED;
switch(gs.player->state)
{
case CHARACTER_WALKING:
player_armature.go_to_animation = MD_S8Lit("Running");
break;
case CHARACTER_IDLE:
player_armature.go_to_animation = MD_S8Lit("Idle");
break;
case CHARACTER_KILLED:
player_armature.go_to_animation = MD_S8Lit("Die Backwards");
player_armature.next_animation_isnt_looping = true;
break;
}
switch (gs.player->state)
{
case CHARACTER_WALKING:
speed = PLAYER_SPEED;
if (LenV2(movement) == 0.0)
{
gs.player->state = CHARACTER_IDLE;
}
else
{
}
break;
case CHARACTER_IDLE:
if (LenV2(movement) > 0.01)
gs.player->state = CHARACTER_WALKING;
break;
case CHARACTER_KILLED:
break;
}
} // not time stopped
if(!gs.player->killed) speed = PLAYER_SPEED;
// velocity processing
{
gs.player->last_moved = NormV2(movement);
@ -6844,7 +6763,6 @@ ISANERROR("Don't know how to do this stuff on this platform.")
pressed = (PressedState) { 0 };
memset(keypressed, 0, sizeof(keypressed));
interact = false;
} // while loop
last_frame_gameplay_processing_time = stm_sec(stm_diff(stm_now(), time_start_gameplay_processing));
@ -6906,17 +6824,6 @@ ISANERROR("Don't know how to do this stuff on this platform.")
draw_quad((DrawParams) { quad_centered(interacting_with->pos, V2(TILE_SIZE, TILE_SIZE)), image_hovering_circle, full_region(image_hovering_circle), WHITE, .layer = LAYER_UI });
}
if (gs.player->state == CHARACTER_WALKING)
{
}
else if (gs.player->state == CHARACTER_IDLE)
{
}
else
{
assert(false); // unknown character state? not defined how to draw
}
// hurt vignette
if (gs.player->damage > 0.0)
{
@ -7009,7 +6916,7 @@ ISANERROR("Don't know how to do this stuff on this platform.")
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(pressed.interact)
if(should_be_visible && pressed.interact)
{
begin_text_input();
}
@ -7556,15 +7463,6 @@ void event(const sapp_event *e)
pressed.interact = true;
}
if (e->key_code == SAPP_KEYCODE_S)
{
pressed.speak_shortcut = true;
}
if (e->key_code == SAPP_KEYCODE_G)
{
pressed.give_shortcut = true;
}
if (e->key_code == SAPP_KEYCODE_LEFT_SHIFT)
{
learned_shift += 0.15f;
@ -7597,7 +7495,7 @@ void event(const sapp_event *e)
profiling = !profiling;
if (profiling)
{
init_profiling("rpgpt.spall");
init_profiling(PROFILING_SAVE_FILENAME);
init_profiling_mythread(0);
}
else

@ -151,20 +151,12 @@ typedef struct EntityRef
int generation;
} EntityRef;
typedef enum CharacterState
{
CHARACTER_WALKING,
CHARACTER_IDLE,
CHARACTER_KILLED,
} CharacterState;
typedef enum
{
STANDING_INDIFFERENT,
STANDING_JOINED,
} NPCPlayerStanding;
typedef Vec4 Color;
typedef BUFF(Vec2, MAX_ASTAR_NODES) AStarPath;
@ -219,7 +211,6 @@ typedef struct Entity
// the kinds are at the top so you can quickly see what kind an entity is in the debugger
bool is_world; // the static world. An entity is always returned when you collide with something so support that here
bool is_npc;
bool is_character;
// fields for all gs.entities
Vec2 pos;
@ -262,15 +253,8 @@ typedef struct Entity
int gen_request_id;
Vec2 target_goto;
// character
bool waiting_on_speech_with_somebody;
EntityRef interacting_with; // for drawing outline on maybe interacting with somebody
Vec2 to_throw_direction;
BUFF(Vec2, 8) position_history; // so npcs can follow behind the player
CharacterState state;
EntityRef talking_to;
} Entity;
@ -408,7 +392,7 @@ MD_String8 generate_chatgpt_prompt(MD_Arena *arena, GameState *gs, Entity *e, Ca
AddFmt("The characters who are near you, that you can target:\n");
BUFF_ITER(Entity*, &can_talk_to)
{
assert((*it)->is_npc || (*it)->is_character);
assert((*it)->is_npc);
MD_String8 info = MD_S8Lit("");
if((*it)->killed)
{
@ -575,7 +559,7 @@ MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 action_
{
error_message = FmtWithLint(arena, "Speech string provided is too big, maximum bytes is %d", MAX_SENTENCE_LENGTH);
}
assert(!e->is_character); // player can't perform AI actions?
assert(e->npc_kind != NPC_Player); // player can't perform AI actions?
if(error_message.size == 0)
{

@ -5,58 +5,13 @@
@ctype vec3 Vec3
@ctype vec2 Vec2
@block inverse_functions
// Webgl 1 doesn't have inverse() but we need it, pulled from https://github.com/glslify/glsl-inverse/blob/master/index.glsl
mat4 my_inverse(mat4 m) {
float
a00 = m[0][0], a01 = m[0][1], a02 = m[0][2], a03 = m[0][3],
a10 = m[1][0], a11 = m[1][1], a12 = m[1][2], a13 = m[1][3],
a20 = m[2][0], a21 = m[2][1], a22 = m[2][2], a23 = m[2][3],
a30 = m[3][0], a31 = m[3][1], a32 = m[3][2], a33 = m[3][3],
b00 = a00 * a11 - a01 * a10,
b01 = a00 * a12 - a02 * a10,
b02 = a00 * a13 - a03 * a10,
b03 = a01 * a12 - a02 * a11,
b04 = a01 * a13 - a03 * a11,
b05 = a02 * a13 - a03 * a12,
b06 = a20 * a31 - a21 * a30,
b07 = a20 * a32 - a22 * a30,
b08 = a20 * a33 - a23 * a30,
b09 = a21 * a32 - a22 * a31,
b10 = a21 * a33 - a23 * a31,
b11 = a22 * a33 - a23 * a32,
det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
return mat4(
a11 * b11 - a12 * b10 + a13 * b09,
a02 * b10 - a01 * b11 - a03 * b09,
a31 * b05 - a32 * b04 + a33 * b03,
a22 * b04 - a21 * b05 - a23 * b03,
a12 * b08 - a10 * b11 - a13 * b07,
a00 * b11 - a02 * b08 + a03 * b07,
a32 * b02 - a30 * b05 - a33 * b01,
a20 * b05 - a22 * b02 + a23 * b01,
a10 * b10 - a11 * b08 + a13 * b06,
a01 * b08 - a00 * b10 - a03 * b06,
a30 * b04 - a31 * b02 + a33 * b00,
a21 * b02 - a20 * b04 - a23 * b00,
a11 * b07 - a10 * b09 - a12 * b06,
a00 * b09 - a01 * b07 + a02 * b06,
a31 * b01 - a30 * b03 - a32 * b00,
a20 * b03 - a21 * b01 + a22 * b00) / det;
}
@end
// for this block, define a variable called `model_space_pos` to be used as an input
@block vs_compute_light_output
world_space_frag_pos = model * vec4(model_space_pos, 1.0);
vec4 frag_pos = view * world_space_frag_pos;
//@Speed I think we can just take the third row here and be fine.
light_dir = normalize(my_inverse(directional_light_space_matrix) * vec4(0.0, 0.0, -1.0, 0.0)).xyz;
light_dir = normalize(inverse(directional_light_space_matrix) * vec4(0.0, 0.0, -1.0, 0.0)).xyz;
light_space_fragment_position = directional_light_space_matrix * vec4(world_space_frag_pos.xyz, 1.0);
@ -92,8 +47,6 @@ float decode_normalized_float32(vec4 v)
return sign * (v.z*255.0 + v.y);
}
@include_block inverse_functions
void main() {
vec4 total_position = vec4(0.0f);
@ -151,8 +104,6 @@ uniform vs_params {
vec3 wobble_world_source;
};
@include_block inverse_functions
void main() {
//vec3 transformed_pos = vec3(pos_in.x, pos_in.y + sin(pos_in.x * 5.0 + pos_in.y * 9.0 + time*1.9)*0.045, pos_in.z);
@ -498,6 +449,8 @@ vec3 acesFilm(const vec3 x) {
return clamp((x * (a * x + b)) / (x * (c * x + d ) + e), 0.0, 1.0);
}
@include tuning.h
void main() {
// clip space is from [-1,1] [left, right]of screen on X, and [-1,1] [bottom, top] of screen on Y
if(pos.x < clip_ul.x || pos.x > clip_lr.x || pos.y < clip_lr.y || pos.y > clip_ul.y) discard;
@ -510,23 +463,36 @@ void main() {
col.rgb = acesFilm(col.rgb);
// Film grain
#if (FILM_GRAIN_STRENGTH > 0)
{ // Film grain
vec2 grain_uv = gl_FragCoord.xy / screen_size.xy;
float x = grain_uv.x * grain_uv.y * time * 24 + 100.0;
vec3 noise = vec3(mod((mod(x, 13.0) + 1.0) * (mod(x, 123.0) + 1.0), 0.01)) * 100.0;
col.rgb += (noise - 0.5) * 0.05;
col.rgb *= 0.95;
// Hard-clip contrast levels
float min = 11; float max = 204;
col.rgb += (noise - 0.5) * (FILM_GRAIN_STRENGTH * 0.01);
col.rgb *= (1 - FILM_GRAIN_STRENGTH * 0.01);
}
#endif
#if (CONTRAST_BOOST_MIN > 0 || CONTRAST_BOOST_MAX < 255)
{ // Hard-clip contrast levels
float min = CONTRAST_BOOST_MIN;
float max = CONTRAST_BOOST_MAX;
col.rgb -= min/255;
col.rgb *= 255/(max-min);
// Vignette
col.rgb *= clamp(1.5 - length(gl_FragCoord.xy / screen_size.xy - vec2(0.5)), 0, 1);
// Cross-process
float cross_process_strength = 0.5;
}
#endif
#if (VIGNETTE_STRENGTH > 0)
{ // Vignette
col.rgb *= clamp((2 - VIGNETTE_STRENGTH * 0.01) - length(gl_FragCoord.xy / screen_size.xy - vec2(0.5)), 0, 1);
}
#endif
#if (CROSS_PROCESS_STRENGTH > 0)
{ // Cross-process
float cross_process_strength = CROSS_PROCESS_STRENGTH * 0.01;
col.rg *= (col.rg * ((-cross_process_strength) * col.rg + (-1.5 * (-cross_process_strength))) + (0.5 * (-cross_process_strength) + 1));
col.b *= (col.b * ((+cross_process_strength) * col.b + (-1.5 * (+cross_process_strength))) + (0.5 * (+cross_process_strength) + 1));
col.rgb = clamp(col.rgb, 0, 1);
}
#endif
// col.rgb = clamp(col.rgb, 0, 1);
frag_color = col;
}

@ -1,8 +1,9 @@
Urgent:
- room system where characters can go to rooms. camera constrained to room bounds, and know which rooms are near them to go to
- REALLY look into characters not being able to shoot eachother
- Silent threshold for each character from 0 to 1 that controls random chance after every eavesdropped dialog that the character says nothing in response
- Tag memories from drama as permanent so they're never forgotten and always seed character behavior
- gman character that gives you randomized goals such as (kill so and so) or (befriend blank) or (get john to go to other room), and decides itself with gpt when you're done with the game, when you've 'learned your lesson'.
- room system where characters can go to rooms. camera constrained to room bounds, and know which rooms are near them to go to
- make them understand they're holding their shotgun more
- Fix everything about the shotgun, put shotgun away
- can't interact with characters while they're thinking
@ -15,7 +16,7 @@ Urgent:
- dot dot dot speech bubble to show that they heard your request, but are ignoring you.
Long distance:
- Design character creator
- Design character creator (text input is always a modal, keeps code simple don't have to make a banger line edit)
- Let ChatGPT file bug reports with something like !BugReport(This shouldn't be happening, developer. I killed them and they're still alive)
- Cylindrical colliders for the people
- System where I can upload any AI interaction into gpt playground and inspect the conversation

@ -1,4 +1,5 @@
#pragma once
#ifndef TUNING_H // #pragma once isn't supported by sokol-shdc yet
#define TUNING_H
#define LEVEL_TILES 150 // width and height of level tiles array
#define LAYERS 3
@ -28,6 +29,7 @@
#define BIG_ARENA_SIZE (ARENA_SIZE * 4)
#ifdef DEVTOOLS
#define PROFILING_SAVE_FILENAME "rpgpt.spall"
// server url cannot have trailing slash
//#define MOCK_AI_RESPONSE
#define SERVER_DOMAIN "localhost"
@ -71,3 +73,20 @@
#define NEAR_PLANE_DISTANCE (0.01f)
#define FAR_PLANE_DISTANCE (45.0f)
#define SHADOW_MAP_DIMENSION (2048)
// Post-processing
#if 0 // use this to completely disable
#define FILM_GRAIN_STRENGTH 0 // 0 to 100
#define CONTRAST_BOOST_MIN 0 // 0 to 255
#define CONTRAST_BOOST_MAX 255 // 0 to 255
#define VIGNETTE_STRENGTH 0 // 0 to 100
#define CROSS_PROCESS_STRENGTH 0 // 0 to 100
#else
#define FILM_GRAIN_STRENGTH 0 // 0 to 100
#define CONTRAST_BOOST_MIN 11 // 0 to 255
#define CONTRAST_BOOST_MAX 204 // 0 to 255
#define VIGNETTE_STRENGTH 50 // 0 to 100
#define CROSS_PROCESS_STRENGTH 50 // 0 to 100
#endif
#endif // TUNING_H

Loading…
Cancel
Save