Entropie git · main
Entropie festival game; first steps
C 88.5% Markdown 9.1% GLSL 2.3%git clone https://git.christianimmanuel.de/games/Entropie.gitwget https://git.christianimmanuel.de/games/Entropie/archive/Entropie.tar.gzentropie/src/world/object.c raw
#include "world/object.h"
#include "world/stage.h"
#include "world/car.h"
#include <stdio.h>
#include <stdlib.h>
#include <SDL3/SDL.h>
#define DEFAULT_STRENGTH 100.0f
#define CYLINDER_SEGMENTS 30
#define ROOF_PITCH (5.0f / 12.0f) /* roof height per building width */
#define ROOF_THICKNESS 0.4f /* roof collider slab, below the surface */
#define HALL_PITCH 0.14f /* farm halls have shallow roofs */
#define BEER_PER_SIP 0.07f
#define VODKA_SIPS 8
#define VODKA_PER_SIP 0.16f
#define BOTTLE_FALL_TIME 0.35f
#define TREE_FALL_TIME 2.4f
#define TRUNK_RADIUS 0.2f
#define BIG_TRUNK 0.5f /* the big old tree (variant 2): a metre through */
#define WOOD_DENSITY 500.0f /* kg per cubic meter */
#define WOODPILE_BASE 0.08f /* height of the sleepers */
#define SPRINT_SPEED 6.6f /* an average person flat out (m/s) */
#define JUMP_SPEED 3.0f /* about 0.45 m off the ground */
static void addCollider(Object *obj, Vec3 center, Vec3 size, Vec3 tilt) {
if (obj->collider_count >= OBJECT_MAX_COLLIDERS) return;
obj->colliders[obj->collider_count++] = (Collider){
.center = center,
.half_extents = vec3_scale(size, 0.5f),
.tilt = tilt,
};
}
/* Walls block plus one thin slab lying under each roof slope. */
static void setupBuilding(Object *obj, BuildingSize b) {
addCollider(obj, vec3(0.0f, b.wall_height * 0.5f, 0.0f),
vec3(b.length, b.wall_height, b.width), vec3_zero());
const float half_w = b.width * 0.5f;
const float slope = sqrtf(half_w * half_w + b.roof_height * b.roof_height);
const float angle = atanf(b.roof_height / half_w);
const Vec3 size = vec3(b.length, ROOF_THICKNESS, slope);
for (int side = -1; side <= 1; side += 2) {
/* side = -1: back slope (-Z), side = +1: front slope (+Z) */
Vec3 normal = vec3(0.0f, cosf(angle), (float)side * sinf(angle));
Vec3 surface = vec3(0.0f, b.wall_height + b.roof_height * 0.5f, (float)side * half_w * 0.5f);
Vec3 center = vec3_sub(surface, vec3_scale(normal, ROOF_THICKNESS * 0.5f));
addCollider(obj, center, size, vec3((float)-side * angle, 0.0f, 0.0f));
}
}
/* Trunk plus a box around the canopy. */
#define TREE_LAYERS 8
static TreeSize treeSize(const Object *obj) {
return (TreeSize){ .trunk_height = obj->size.y, .trunk_radius = obj->variant == 2 ? BIG_TRUNK : obj->variant ? TRUNK_RADIUS * 0.8f : TRUNK_RADIUS,
.leaf_layers = obj->branches > 0 ? obj->branches : 1, .leaf_radius = obj->size.x * 0.5f,
.fruit = obj->variant == 1, .cut = obj->trunk_cut, .old = obj->variant == 2,
.seed = Shapes_treeSeed(obj->position.x, obj->position.z) };
}
static void setupTree(Object *obj, TreeSize t) {
const float canopy_h = Shapes_treeHeight(t) - t.trunk_height;
const float canopy_w = t.leaf_radius * 1.2f;
addCollider(obj, vec3(0.0f, t.trunk_height * 0.5f, 0.0f),
vec3(t.trunk_radius * 2.0f, t.trunk_height, t.trunk_radius * 2.0f), vec3_zero());
addCollider(obj, vec3(0.0f, t.trunk_height + canopy_h * 0.5f, 0.0f),
vec3(canopy_w, canopy_h, canopy_w), vec3_zero());
}
/* Firewood: a half or a quarter of a log, centered on its box. */
static void setupFirewood(Object *obj, float diameter, float length, int parts) {
const float r = diameter * 0.5f;
obj->mesh = Shapes_firewood(diameter, length, parts);
obj->pivot = vec3(0.0f, length * 0.5f, 0.0f);
obj->size = vec3(diameter, length, (float)parts);
obj->weight = WOOD_DENSITY * PI_F * r * r * length / (float)parts;
obj->collider_count = 0;
addCollider(obj, vec3_zero(), vec3(parts == 2 ? diameter : r, length, r), vec3_zero());
}
static float flatGround(const void *ctx, float x, float z) {
(void)x;
(void)z;
return *(const float *)ctx;
}
static Mesh decalMesh(ObjectType type, Palette palette, Vec3 position, Vec3 size, ShapeGround ground) {
unsigned seed = (unsigned)(int)(position.x * 811.0f) ^ ((unsigned)(int)(position.z * 677.0f) << 10);
(void)palette;
return type == OBJ_STINK ? Shapes_stink(size.x, seed, ground) : Shapes_vomit(size.x, seed, ground);
}
Object *Object_create(ObjectType type, Palette palette, Vec3 position, Vec3 size, float weight) {
Object *obj = calloc(1, sizeof(*obj));
if (!obj) return NULL;
obj->type = type;
obj->palette = palette;
obj->make_arg = weight;
obj->hands.drink_hand = -1;
obj->cast_id = -1;
obj->work_state.since = 1e3f; /* hasn't worked yet */
obj->main_hand = HAND_RIGHT;
obj->top_speed = SPRINT_SPEED;
obj->fitness = 0.5f;
obj->wits = 0.5f;
obj->height_fear = 0.3f;
obj->jump_speed = JUMP_SPEED;
obj->tap_timer = 1e3f;
obj->position = position;
obj->weight = weight;
obj->strength = DEFAULT_STRENGTH;
obj->tolerance = 1.0f;
obj->size = size;
obj->needs = (Needs){ .stamina = 1.0f, .hunger = 0.25f, .thirst = 0.25f, .bladder = 0.1f, .sleepiness = 0.2f };
/* Centered meshes: pivot in the middle of the bounding box. */
const Vec3 centered = vec3(0.0f, size.y * 0.5f, 0.0f);
switch (type) {
case OBJ_BOX:
case OBJ_BOX_STATIC:
obj->mesh = Shapes_box(size, palette);
obj->pivot = centered;
obj->is_static = (type == OBJ_BOX_STATIC);
addCollider(obj, vec3_zero(), size, vec3_zero());
break;
case OBJ_CYLINDER:
case OBJ_CYLINDER_STATIC:
obj->mesh = Shapes_cylinder(size.x, size.y, CYLINDER_SEGMENTS, palette);
obj->pivot = centered;
obj->is_static = (type == OBJ_CYLINDER_STATIC);
addCollider(obj, vec3_zero(), size, vec3_zero());
break;
case OBJ_STUMP: {
obj->mesh = Shapes_stump(size.x, size.y, Shapes_treeSeed(position.x, position.z)); /* (the tree's own foot) */
obj->pivot = centered;
obj->is_static = true;
addCollider(obj, vec3_zero(), vec3(size.x, size.y, size.x), vec3_zero());
break;
}
case OBJ_HUMAN:
case OBJ_PLAYER:
obj->pivot = centered;
obj->is_character = true;
obj->step_height = size.y / 3.0f;
obj->look = Figure_wardrobe(0);
obj->look.height = size.y;
addCollider(obj, vec3_zero(), size, vec3_zero());
break;
case OBJ_BOTTLE:
case OBJ_VODKA:
case OBJ_WATER:
obj->mesh = Shapes_bottle(size.x, size.y, type == OBJ_VODKA ? BOTTLE_SPIRITS
: type == OBJ_WATER ? BOTTLE_WATER : BOTTLE_BEER);
obj->pivot = centered;
obj->sips = type == OBJ_VODKA ? VODKA_SIPS : BOTTLE_SIPS;
obj->alcohol = type == OBJ_VODKA ? VODKA_PER_SIP : (type == OBJ_BOTTLE ? BEER_PER_SIP : 0.0f);
obj->quench = type == OBJ_WATER ? 0.2f : (type == OBJ_BOTTLE ? 0.12f : 0.02f);
obj->refresh = type == OBJ_WATER ? 0.15f : (type == OBJ_BOTTLE ? 0.08f : 0.03f);
addCollider(obj, vec3_zero(), vec3(size.x, size.y, size.x), vec3_zero());
break;
case OBJ_TOY: /* (weight picks which: see object.h) */
obj->variant = (int)weight;
obj->weight = obj->variant == 0 ? 0.15f : 0.3f;
obj->mesh = Shapes_toy(obj->variant, size);
obj->pivot = centered;
addCollider(obj, vec3_zero(), obj->variant == 0 ? vec3(size.x, size.x, size.x) : vec3(size.x, size.y, size.y), vec3_zero());
break;
case OBJ_APPLE:
obj->mesh = Shapes_apple(size.x);
obj->pivot = vec3(0.0f, size.x * 0.5f, 0.0f);
obj->sips = 3;
obj->nourish = 0.05f;
obj->quench = 0.02f;
obj->refresh = 0.03f;
addCollider(obj, vec3_zero(), vec3(size.x, size.x, size.x), vec3_zero());
break;
case OBJ_BREAD:
obj->mesh = Shapes_bread(size);
obj->pivot = centered;
obj->sips = 5;
obj->nourish = 0.09f;
obj->refresh = 0.04f;
addCollider(obj, vec3_zero(), size, vec3_zero());
break;
case OBJ_SUPPLY: {
bool round = palette == PALETTE_MODERN;
obj->mesh = palette == PALETTE_WARM ? Shapes_appleCrate(size)
: palette == PALETTE_STANDARD ? Shapes_coolBox(size)
: palette == PALETTE_GROUND ? Shapes_bagCarton(size) : Shapes_barrel(size.x, size.y);
obj->pivot = centered;
obj->is_static = weight >= 0.0f; /* (weight < 0: a bought one, -weight kg: carried in from the car) */
if (weight < 0.0f) obj->weight = -weight;
obj->dispenses = palette == PALETTE_WARM ? OBJ_APPLE : palette == PALETTE_STANDARD ? OBJ_SAUSAGE
: palette == PALETTE_GROUND ? OBJ_BAG : OBJ_WATER;
addCollider(obj, vec3_zero(), round ? vec3(size.x, size.y, size.x) : size, vec3_zero());
break;
}
case OBJ_SAUSAGE:
obj->mesh = Shapes_sausage(size.y, false);
obj->pivot = centered;
obj->sips = 3;
obj->nourish = 0.02f; /* raw: hardly worth it */
obj->refresh = 0.01f;
addCollider(obj, vec3_zero(), vec3(0.04f, size.y, 0.04f), vec3_zero());
break;
case OBJ_FIREPLACE:
obj->mesh = Shapes_fireplace(size.x);
obj->flames = Shapes_flames();
obj->grill_mesh = Shapes_grill();
obj->is_static = true;
obj->pivot = vec3_zero();
/* Low enough to step over; wood put in rests on the ashes. Inside the
ring of stones (its corners at the stones: a square as wide as the
ring stuck out past them, and people beside the fire stepped on and
off its corners, jittering) */
addCollider(obj, vec3(0.0f, 0.03f, 0.0f), vec3(size.x * 0.6f, 0.06f, size.x * 0.6f), vec3_zero());
break;
case OBJ_TRASH:
obj->variant = (int)palette;
obj->size = size.x < 0.0f ? vec3(0.03f, 0.012f, 0.012f) : Shapes_trashSize(palette); /* (a cigarette butt) */
obj->mesh = size.x < 0.0f ? Shapes_butt() : Shapes_trash(palette, (unsigned)(position.x * 131.0f + position.z * 71.0f));
obj->pivot = vec3(0.0f, obj->size.y * 0.5f, 0.0f);
addCollider(obj, vec3_zero(), obj->size, vec3_zero());
break;
case OBJ_BAG:
obj->size = Shapes_bagSize(0.0f);
obj->mesh = Shapes_bag(0.0f);
obj->pivot = vec3(0.0f, obj->size.y * 0.5f, 0.0f);
addCollider(obj, vec3_zero(), obj->size, vec3_zero());
break;
case OBJ_BUSH:
obj->mesh = Shapes_bush(size.x, size.y, (unsigned)(position.x * 37.0f) ^ (unsigned)(position.z * 91.0f));
obj->is_static = true;
addCollider(obj, vec3(0.0f, size.y * 0.45f, 0.0f), vec3(size.x * 0.8f, size.y * 0.9f, size.x * 0.8f), vec3_zero());
break;
case OBJ_FURNITURE:
obj->variant = (int)weight; /* the kind, see Object_createFurniture */
obj->shown = -1;
obj->weight = 100.0f;
obj->mesh = Shapes_furniture((FurnitureKind)obj->variant, size);
obj->is_static = true;
addCollider(obj, vec3(0.0f, (obj->variant == FURN_CHAIR || obj->variant == FURN_BENCH ? SEAT_HEIGHT : size.y) * 0.5f, 0.0f),
vec3(size.x, obj->variant == FURN_CHAIR || obj->variant == FURN_BENCH ? SEAT_HEIGHT : size.y, size.z), vec3_zero());
break;
case OBJ_HOSE:
obj->mesh = Shapes_hoseNozzle();
obj->pivot = vec3(0.0f, 0.1f, 0.0f);
addCollider(obj, vec3_zero(), vec3(0.05f, 0.22f, 0.05f), vec3_zero());
break;
case OBJ_CAN:
obj->mesh = Shapes_wateringCan();
obj->pivot = vec3(0.0f, 0.14f, 0.0f);
addCollider(obj, vec3(0.0f, 0.0f, 0.05f), vec3(0.26f, 0.28f, 0.4f), vec3_zero());
break;
case OBJ_STONE:
obj->mesh = Shapes_stone(size, (unsigned)(position.x * 977.0f) ^ ((unsigned)(position.z * 311.0f) << 5));
obj->pivot = vec3(0.0f, size.y * 0.5f, 0.0f);
addCollider(obj, vec3_zero(), vec3_scale(size, 0.85f), vec3_zero());
break;
case OBJ_LADDER: {
float len = Shapes_ladderLength(0.0f);
obj->mesh = Shapes_ladder(0.0f);
obj->pivot = vec3_zero();
addCollider(obj, vec3(0.0f, len * 0.5f, 0.0f), vec3(LADDER_WIDTH + 0.06f, len, 0.08f), vec3_zero());
break;
}
case OBJ_DOG:
obj->mesh = Shapes_dogBody();
obj->look.height = 0.4f;
addCollider(obj, vec3(0.0f, 0.18f, -0.05f), vec3(0.2f, 0.34f, 0.7f), vec3_zero());
break;
case OBJ_BIN:
obj->mesh = Shapes_bin(size);
obj->pivot = centered;
obj->is_static = true;
addCollider(obj, vec3_zero(), size, vec3_zero());
break;
case OBJ_TAPE:
obj->mesh = Shapes_tapeRoll();
obj->pivot = vec3(0.0f, 0.025f, 0.0f);
addCollider(obj, vec3_zero(), vec3(0.1f, 0.05f, 0.1f), vec3_zero());
break;
case OBJ_URINELLA: /* (never made: they're only counted, in pockets) */
Object_destroy(obj);
return NULL;
case OBJ_SHEARS:
obj->mesh = Shapes_shears(size.y);
obj->pivot = centered;
addCollider(obj, vec3_zero(), vec3(0.06f, size.y, 0.03f), vec3_zero());
break;
case OBJ_CHAINSAW:
obj->mesh = Shapes_chainsaw();
obj->pivot = vec3(0.0f, 0.09f, 0.0f); /* (the motor's underside on the ground) */
addCollider(obj, vec3(0.0f, 0.03f, 0.02f), vec3(0.18f, 0.24f, 0.74f), vec3_zero());
break;
case OBJ_LOG:
obj->mesh = Shapes_log(size.x, size.y);
obj->pivot = centered;
obj->weight = weight > 0.0f ? weight : WOOD_DENSITY * PI_F * size.x * size.x * 0.25f * size.y;
addCollider(obj, vec3_zero(), vec3(size.x, size.y, size.x), vec3_zero());
break;
case OBJ_FIREWOOD:
setupFirewood(obj, size.x, size.y, size.z >= 4.0f ? 4 : 2);
break;
case OBJ_WOODPILE:
obj->mesh = Shapes_woodpile(size.x, size.z);
obj->pivot = vec3(0.0f, WOODPILE_BASE * 0.5f, 0.0f);
obj->is_static = true;
addCollider(obj, vec3_zero(), vec3(size.x, WOODPILE_BASE, size.z), vec3_zero());
break;
case OBJ_BRANCH:
obj->mesh = Shapes_branch(size.y);
obj->pivot = centered;
addCollider(obj, vec3_zero(), vec3(0.2f, size.y, 0.2f), vec3_zero());
break;
case OBJ_PUDDLE:
case OBJ_STINK: {
ShapeGround flat = { .height = flatGround, .ctx = &position.y, .origin = position };
obj->mesh = decalMesh(type, palette, position, size, flat);
obj->is_static = true;
obj->work = 1.0f;
break;
}
case OBJ_CAR: { /* (see car.c Car_create) */
const float back = -size.z * 0.5f;
obj->mesh = Shapes_car(CAR_COLOR, false);
obj->inside = Shapes_carInside(CAR_COLOR);
obj->is_static = true;
obj->weight = 1300.0f;
addCollider(obj, vec3(0.0f, 0.62f, 0.0f), vec3(size.x, 0.74f, size.z), vec3_zero());
addCollider(obj, vec3(0.0f, 1.27f, (back + 0.05f + 0.5f) * 0.5f), vec3(size.x * 0.9f, 0.6f, 0.5f - back - 0.05f), vec3_zero());
break;
}
case OBJ_CRATE:
obj->variant = (int)(weight + 0.5f) < 0 ? 0 : (int)(weight + 0.5f) > 3 ? 3 : (int)(weight + 0.5f);
obj->size = vec3(CRATE_SIZE_X, CRATE_SIZE_Y, CRATE_SIZE_Z);
Crate_fill(obj); /* (full of its own bottles: stored, weight) */
obj->mesh = Shapes_crate(obj->variant, obj->crate_slot, Crate_bottles(obj->variant));
obj->shown = Crate_look(obj);
obj->pivot = vec3(0.0f, CRATE_SIZE_Y * 0.5f, 0.0f);
addCollider(obj, vec3_zero(), obj->size, vec3_zero());
break;
case OBJ_GEAR: { /* kind = weight; its colour: see car.c */
static const Vec3 SIZE[] = { { 0.42f, 0.26f, 0.26f }, { 0.6f, 0.18f, 0.18f }, { 0.5f, 0.16f, 0.16f }, { 0.6f, 0.2f, 0.2f } };
static const float KG[] = { 1.6f, 0.6f, 2.4f, 4.5f };
obj->variant = (int)weight & 3;
obj->size = SIZE[obj->variant];
obj->weight = KG[obj->variant];
obj->mesh = Shapes_gear(obj->variant, vec3(0.5f, 0.5f, 0.5f));
obj->pivot = vec3(0.0f, obj->size.y * 0.5f, 0.0f);
addCollider(obj, vec3_zero(), obj->size, vec3_zero());
break;
}
case OBJ_UMBRELLA: {
static const Vec3 COLORS[] = { { 0.15f, 0.20f, 0.42f }, { 0.70f, 0.14f, 0.16f }, { 0.20f, 0.32f, 0.22f } };
obj->size = vec3(0.08f, UMBRELLA_LENGTH, 0.08f);
obj->weight = 0.5f;
obj->variant = (int)weight % 3;
obj->mesh = Shapes_umbrella(UMBRELLA_LENGTH, UMBRELLA_RADIUS, UMBRELLA_FOLDED, COLORS[obj->variant]);
obj->pivot = vec3(0.0f, UMBRELLA_LENGTH * 0.5f, 0.0f);
addCollider(obj, vec3_zero(), obj->size, vec3_zero());
break;
}
case OBJ_STAGE: /* (world/stage.c: its look by how far it's built; the deck solid once it's in) */
obj->variant = (int)weight;
obj->weight = 500.0f;
obj->is_static = true;
obj->size = size;
obj->mesh = Shapes_stage(0, 0.0f, size.x, size.z, vec3(0.5f, 0.5f, 0.5f));
addCollider(obj, vec3(0.0f, STAGE_DECK * 0.5f, 0.0f), vec3(size.x, STAGE_DECK, size.z), vec3_zero());
obj->collider_count = 0;
break;
case OBJ_MATERIAL: { /* kind = weight */
static const Vec3 SIZE[MATERIAL_KINDS] = { { 0.6f, 0.18f, 0.24f }, /* (deck boards cut to length: short enough to carry past things) */ { 0.16f, 0.09f, 0.11f }, { 0.16f, 0.09f, 0.11f }, { 0.27f, 0.3f, 0.27f } };
static const float KG[MATERIAL_KINDS] = { 17.0f, 1.0f, 1.0f, 6.0f };
obj->variant = (int)weight < 0 ? 0 : (int)weight >= MATERIAL_KINDS ? MATERIAL_KINDS - 1 : (int)weight;
obj->size = SIZE[obj->variant];
obj->weight = KG[obj->variant];
obj->mesh = Shapes_material(obj->variant, obj->size);
obj->pivot = vec3(0.0f, obj->size.y * 0.5f, 0.0f);
addCollider(obj, vec3_zero(), obj->size, vec3_zero());
break;
}
case OBJ_SCYTHE:
obj->size = vec3(0.06f, 1.6f, 0.06f);
obj->weight = 2.5f;
obj->mesh = Shapes_scythe(1.6f);
obj->pivot = vec3(0.0f, 0.8f, 0.0f);
addCollider(obj, vec3_zero(), obj->size, vec3_zero());
break;
case OBJ_SNAIL:
obj->variant = (int)weight ? 1 : 0;
obj->size = vec3(0.05f, 0.045f, 0.08f);
obj->weight = 0.02f;
obj->mesh = Shapes_snail();
obj->pivot = vec3_zero();
addCollider(obj, vec3(0.0f, 0.022f, 0.0f), obj->size, vec3_zero());
break;
case OBJ_ROPE:
obj->size = vec3(0.32f, 0.1f, 0.32f);
obj->weight = 0.6f;
obj->mesh = Shapes_throwBag();
obj->pivot = vec3(0.0f, 0.05f, 0.0f);
addCollider(obj, vec3(0.0f, 0.05f, 0.0f), vec3(0.12f, 0.1f, 0.12f), vec3_zero());
snprintf(obj->name, sizeof obj->name, "Throw line");
break;
case OBJ_SCULPTURE:
obj->variant = (int)weight % 3;
obj->size = size;
obj->weight = 200.0f;
obj->is_static = true;
obj->mesh = Shapes_sculpture(obj->variant, size.x, size.y);
obj->pivot = vec3_zero();
addCollider(obj, vec3(0.0f, size.y * 0.5f, 0.0f), vec3(size.x, size.y, size.x), vec3_zero());
break;
case OBJ_HAT: /* (grey here: its colour once it's known - world.c, showProps) */
obj->size = vec3(0.21f, 0.1f, 0.24f);
obj->weight = 0.1f;
obj->variant = (int)weight == 2 ? 2 : 1;
obj->mesh = Shapes_hat(obj->variant, vec3(0.5f, 0.5f, 0.5f));
addCollider(obj, vec3(0.0f, 0.0f, 0.02f), obj->size, vec3_zero());
break;
case OBJ_TENT: { /* (see tent.c: its mesh by how far it's up) */
obj->variant = weight > 0.5f ? 1 : 0;
obj->weight = 50.0f;
obj->is_static = true;
obj->shown = -1;
obj->size = vec3(TENT_W(obj->variant), TENT_H(obj->variant), TENT_L(obj->variant));
obj->mesh = Shapes_tent(obj->variant, 0, 0.0f, 0, vec3(0.9f, 0.86f, 0.62f), vec3(0.3f, 0.5f, 0.3f));
addCollider(obj, vec3(0.0f, 0.02f, 0.0f), vec3(obj->size.x, 0.04f, obj->size.z), vec3_zero());
break;
}
case OBJ_CLOTHES: /* (made in clothes.c, its mesh given) */
obj->is_static = true;
break;
case OBJ_WALL: /* nothing to see: the building's OBJ_STRUCTURE shows it */
obj->pivot = vec3(0.0f, size.y * 0.5f, 0.0f);
obj->is_static = true;
obj->weight = 1000.0f;
addCollider(obj, vec3_zero(), size, vec3_zero());
break;
case OBJ_STRUCTURE:
case OBJ_DECAL: /* made with its mesh by Object_createDecal */
obj->is_static = true;
break;
case OBJ_AXE:
obj->mesh = Shapes_axe(size.y);
obj->pivot = centered;
addCollider(obj, vec3(0.0f, 0.0f, 0.03f), vec3(0.04f, size.y, 0.16f), vec3_zero());
break;
case OBJ_TREE: {
obj->variant = palette == PALETTE_STANDARD; /* a fruit tree */
obj->branches = TREE_LAYERS;
TreeSize t = treeSize(obj);
obj->mesh = Shapes_tree(t);
obj->is_static = true;
setupTree(obj, t);
break;
}
case OBJ_HOUSE:
case OBJ_BARN: {
BuildingSize b = { .length = size.x, .width = size.z, .wall_height = size.y,
.look = type == OBJ_HOUSE ? (palette == PALETTE_STANDARD ? BUILDING_PLASTER
: palette == PALETTE_MODERN ? BUILDING_PLASTER_DARK : BUILDING_BRICK)
: palette == PALETTE_MODERN ? BUILDING_HALL_SOLAR
: palette == PALETTE_STANDARD ? BUILDING_HALL_RED
: palette == PALETTE_GROUND ? BUILDING_HALL : BUILDING_BRICK,
.roof_height = size.z * (type == OBJ_HOUSE || palette == PALETTE_WARM ? ROOF_PITCH : HALL_PITCH) };
obj->mesh = Shapes_building(b, type == OBJ_HOUSE);
obj->variant = (int)b.look;
obj->is_static = true;
setupBuilding(obj, b);
break;
}
}
if (!obj->is_character && !obj->mesh.vao && type != OBJ_WALL) {
SDL_Log("Object_create: mesh creation failed (type %d)", (int)type);
Object_destroy(obj);
return NULL;
}
Object_syncColliders(obj);
return obj;
}
Object *Object_createOnGround(ObjectType type, Palette palette, Vec3 position, Vec3 size, ShapeGround ground) {
Object *obj = Object_create(type, palette, position, size, 0.0f);
if (!obj) return NULL;
ground.origin = position;
Mesh mesh = decalMesh(type, palette, position, size, ground);
if (mesh.vao) {
Mesh_destroy(&obj->mesh);
obj->mesh = mesh;
}
return obj;
}
Object *Object_createStructure(Mesh mesh, Vec3 position, float yaw, Vec3 size) {
Object *obj = Object_createDecal(mesh, position);
if (!obj) return NULL;
obj->type = OBJ_STRUCTURE;
obj->size = size; /* (big: always drawn) */
obj->rotation = vec3(0.0f, yaw, 0.0f);
return obj;
}
Object *Object_createDecal(Mesh mesh, Vec3 position) {
Object *obj = calloc(1, sizeof(*obj));
if (!obj) {
Mesh_destroy(&mesh);
return NULL;
}
obj->type = OBJ_DECAL;
obj->position = position;
obj->mesh = mesh;
obj->is_static = true;
obj->hands.drink_hand = -1;
obj->cast_id = -1;
return obj;
}
#define STINK_RADIUS 2.2f /* how far a fresh puddle can be smelled */
static Object **g_kinds[KIND_COUNT];
static int g_kind_n[KIND_COUNT], g_kind_cap[KIND_COUNT];
static Object *const *g_kind_src;
static int g_kind_count = -1;
void Object_indexKinds(Object *const *objects, int count) {
g_kind_src = NULL;
for (int k = 0; k < KIND_COUNT; k++) {
g_kind_n[k] = 0;
if (g_kind_cap[k] < count) {
Object **grown = realloc(g_kinds[k], (size_t)(count + 64) * sizeof *grown);
if (!grown) return;
g_kinds[k] = grown;
g_kind_cap[k] = count + 64;
}
}
for (int i = 0; i < count; i++) {
Object *o = objects[i];
int k = o->is_character ? KIND_CHARACTERS : o->type == OBJ_FIREPLACE ? KIND_FIREPLACES : o->type == OBJ_STINK ? KIND_STINKS : -1;
if (k >= 0) g_kinds[k][g_kind_n[k]++] = o;
}
g_kind_src = objects;
g_kind_count = count;
}
Object *const *Object_kind(int kind, Object *const *objects, int count, int *n) {
if (objects && objects == g_kind_src && count == g_kind_count && kind >= 0 && kind < KIND_COUNT) {
*n = g_kind_n[kind];
return g_kinds[kind];
}
*n = count;
return objects;
}
float Object_stinkAt(Object *const *objects, int count, Vec3 pos) {
float worst = 0.0f;
objects = Object_kind(KIND_STINKS, objects, count, &count);
for (int i = 0; i < count; i++) {
const Object *o = objects[i];
if (o->type != OBJ_STINK || o->broken) continue;
float dx = o->position.x - pos.x, dz = o->position.z - pos.z;
float near = 1.0f - sqrtf(dx * dx + dz * dz) / STINK_RADIUS;
if (near > 0.0f) worst = fmaxf(worst, near * o->work);
}
return worst;
}
void Object_destroy(Object *obj) {
if (!obj) return;
Mesh_destroy(&obj->mesh);
Mesh_destroy(&obj->flames);
Mesh_destroy(&obj->grill_mesh);
Mesh_destroy(&obj->inside);
free(obj->load);
free(obj);
}
void Object_setRotation(Object *obj, Vec3 rotation) {
obj->rotation = rotation;
Object_syncColliders(obj);
}
void Object_syncColliders(Object *obj) {
const Mat4 r = mat4_rotate_yxz(obj->rotation);
for (int i = 0; i < obj->collider_count; i++) {
Collider *c = &obj->colliders[i];
Mat4 rc = mat4_mul(r, mat4_rotate_yxz(c->tilt));
c->axes[0] = vec3_normalize(mat4_mul_dir(rc, vec3(1, 0, 0)));
c->axes[1] = vec3_normalize(mat4_mul_dir(rc, vec3(0, 1, 0)));
c->axes[2] = vec3_normalize(mat4_mul_dir(rc, vec3(0, 0, 1)));
c->offset = vec3_add(obj->pivot, mat4_mul_dir(r, c->center));
}
}
Mat4 Object_modelMatrix(const Object *obj) {
Mat4 t = mat4_translate(vec3_add(obj->position, obj->pivot));
return mat4_mul(t, mat4_rotate_yxz(obj->rotation));
}
Vec3 Object_facing(const Object *obj) {
/* Inverse of the player's yaw: rotation.y = atan2(f.z, f.x) + pi/2 */
float a = obj->rotation.y - PI_F / 2.0f;
return vec3(cosf(a), 0.0f, sinf(a));
}
Vec3 Object_colliderCenter(const Object *obj, int index) {
return vec3_add(obj->position, obj->colliders[index].offset);
}
/* (slab test in the box's frame) */
float Collider_ray(const Collider *c, Vec3 center, Vec3 origin, Vec3 dir, Vec3 *normal) {
float t_enter = -1e9f, t_exit = 1e9f, enter_sign = 1.0f;
int enter_axis = -1;
const float half[3] = { c->half_extents.x, c->half_extents.y, c->half_extents.z };
Vec3 rel = vec3_sub(origin, center);
for (int k = 0; k < 3; k++) {
float o = vec3_dot(rel, c->axes[k]), d = vec3_dot(dir, c->axes[k]);
if (fabsf(d) < 1e-6f) {
if (fabsf(o) > half[k]) return -1.0f;
continue;
}
float t1 = (-half[k] - o) / d, t2 = (half[k] - o) / d;
float sign = -1.0f; /* entering through the -half face */
if (t1 > t2) { float t = t1; t1 = t2; t2 = t; sign = 1.0f; }
if (t1 > t_enter) { t_enter = t1; enter_axis = k; enter_sign = sign; }
t_exit = fminf(t_exit, t2);
}
if (t_enter > t_exit || t_enter < 0.0f || enter_axis < 0) return -1.0f;
if (normal) *normal = vec3_scale(c->axes[enter_axis], enter_sign);
return t_enter;
}
float Object_rayToCollider(const Object *obj, int index, Vec3 origin, Vec3 dir, Vec3 *normal) {
return Collider_ray(&obj->colliders[index], Object_colliderCenter(obj, index), origin, dir, normal);
}
float Object_reach(const Object *obj) {
float r = 0.0f;
for (int i = 0; i < obj->collider_count; i++)
r = fmaxf(r, vec3_length(obj->colliders[i].offset) + vec3_length(obj->colliders[i].half_extents));
return r;
}
#define HIGH_UP 1.6f /* m over the ground: up high */
bool Object_upHigh(const Object *obj, float ground_y) {
const Object *on = obj->standing_on;
if (on && on->type == OBJ_WALL && on->variant == 3) return true; /* (a roof) */
return obj->is_grounded && obj->position.y - ground_y > HIGH_UP;
}
float Object_caution(const Object *obj) {
float d = fminf(obj->drunk, 1.0f);
float c = 0.35f + 0.5f * obj->height_fear - 0.15f * obj->fitness; /* (sporty: surer-footed) */
c += d * (obj->wits - 0.45f) * 0.8f; /* drunk: the sharp ones extra careful, the dim ones careless */
return clampf(c, 0.0f, 1.0f);
}
#define WORN_FEEL 0.6f /* worn out from the days before: this much tireder at most */
/* Worn down by the day: how sleepy, felt more by the sluggish, the unfit and
the drunk, hardly by the lively and sporty (0 = fresh .. 1 = done in). */
float Object_tiredness(const Object *obj) {
if (!obj->is_character) return 0.0f;
float sleepy = clampf((obj->needs.sleepiness - 0.25f) / 0.75f, 0.0f, 1.0f);
float feels = (1.0f - 0.35f * clampf(obj->temperament.energy, -1.0f, 1.0f)) * (1.2f - 0.4f * obj->fitness)
+ 0.4f * fminf(obj->drunk, 1.0f);
float worn = WORN_FEEL * obj->worn * (1.15f - 0.3f * obj->fitness); /* (yesterday still in the bones) */
return clampf(sleepy * feels + worn, 0.0f, 1.0f);
}
float Object_vigor(const Object *obj) {
return (0.5f + 0.5f * fminf(obj->needs.stamina / 0.3f, 1.0f)) * (1.0f - 0.25f * Object_tiredness(obj));
}
void Object_setHeight(Object *obj, float height) {
if (!obj->is_character || obj->collider_count == 0 || height < 0.5f) return;
float k = height / fmaxf(obj->size.y, 0.1f);
obj->size = vec3(obj->size.x * k, height, obj->size.z * k);
obj->colliders[0].half_extents = vec3_scale(obj->size, 0.5f);
obj->pivot = vec3(0.0f, height * 0.5f, 0.0f);
obj->step_height = height / 3.0f;
obj->look.height = height;
obj->top_speed *= sqrtf(k);
obj->jump_speed *= sqrtf(k);
Object_syncColliders(obj);
}
float Object_stepUpMost(const Object *obj) {
const float s = obj->look.height / 1.75f;
if (obj->type == OBJ_PLAYER) return 0.75f * s; /* (you decide yourself: as high as anyone, see physics.c STEP_UP_MOST) */
float most = (0.38f + 0.37f * obj->fitness) * (0.75f + 0.25f * Object_vigor(obj)) - 0.25f * fminf(obj->drunk, 1.0f);
return clampf(most, 0.25f, 0.75f) * s;
}
float Object_topSpeed(const Object *obj) {
return obj->top_speed * Object_vigor(obj) * (1.0f - 0.35f * fminf(obj->drunk, 1.0f));
}
float Object_jumpSpeed(const Object *obj) {
return obj->jump_speed * (0.85f + 0.15f * Object_vigor(obj)) * (1.0f - 0.3f * fminf(obj->drunk, 1.0f));
}
void Object_setPerson(Object *obj, const char *name, int main_hand) {
snprintf(obj->name, sizeof(obj->name), "%s", name);
obj->main_hand = main_hand;
/* Light and lively people are quicker on their feet */
float build = 70.0f / fmaxf(obj->weight, 30.0f);
float fit = obj->fitness - 0.5f; /* (sporty people are quicker, whatever their build) */
obj->top_speed = SPRINT_SPEED * powf(build, 0.35f) * (1.0f + 0.12f * obj->temperament.energy) * (1.0f + 0.25f * fit);
obj->jump_speed = JUMP_SPEED * powf(build, 0.3f) * (1.0f + 0.08f * obj->temperament.energy) * (1.0f + 0.15f * fit);
/* Bigger people and those with stamina hold more; and everybody a bit their own */
unsigned h = 2166136261u;
for (const char *c = obj->name; *c; c++) h = (h ^ (unsigned char)*c) * 16777619u;
obj->bladder_size = clampf(1.0f + 0.25f * clampf((obj->weight - 70.0f) / 25.0f, -1.0f, 1.0f)
+ 0.15f * obj->temperament.energy + 0.3f * ((float)(h % 1000u) / 1000.0f - 0.5f), 0.65f, 1.5f);
}
float Object_bladder(const Object *obj) {
float size = obj->bladder_size > 0.1f ? obj->bladder_size : 1.0f;
return size * (0.8f + 0.2f * Object_vigor(obj)); /* worn out: it presses sooner */
}
float Object_peeTime(const Object *obj) {
return clampf(2.5f + 7.0f * obj->needs.bladder * Object_bladder(obj), 2.5f, 12.0f);
}
bool Object_isBottle(const Object *obj) {
return obj->type == OBJ_BOTTLE || obj->type == OBJ_VODKA || obj->type == OBJ_WATER;
}
bool Object_isFood(const Object *obj) {
return obj->type == OBJ_APPLE || obj->type == OBJ_BREAD || obj->type == OBJ_SAUSAGE;
}
bool Object_isTrash(const Object *obj) {
if (!obj->mesh.vao) return false; /* used up */
if (obj->type == OBJ_TRASH) return true;
return Object_isBottle(obj) && (obj->sips <= 0 || obj->broken);
}
bool Object_isFuel(const Object *obj) {
if (!obj->mesh.vao) return false; /* burnt */
return obj->type == OBJ_FIREWOOD || obj->type == OBJ_BRANCH || (obj->type == OBJ_LOG && obj->size.y <= 0.8f);
}
Object *Object_createFurniture(FurnitureKind kind, Vec3 position, Vec3 size, float yaw) {
Object *o = Object_create(OBJ_FURNITURE, PALETTE_WARM, position, size, (float)kind);
if (o) Object_setRotation(o, vec3(0.0f, yaw, 0.0f));
return o;
}
#define GATE_SWING_TIME 1.8f /* s from shut to open (right round) */
void Object_swingGate(Object *g, float dt) {
float goal = g->gate_open ? 1.0f : 0.0f;
g->gate_t += clampf(goal - g->gate_t, -dt / GATE_SWING_TIME, dt / GATE_SWING_TIME);
float u = smoothstepf(0.0f, 1.0f, g->gate_t);
float a = u * PI_F * 0.985f; /* (open: right round, flat against the wall outside) */
Vec3 dir = vec3_normalize(vec3_add(vec3_scale(g->gate_shut, cosf(a)), vec3_scale(g->gate_out, sinf(a))));
/* (folding back it stands off the wall by its own thickness: it lies against it, not in it) */
Vec3 mid = vec3_add(g->hinge, vec3_add(vec3_scale(dir, g->size.x * 0.5f), vec3_scale(g->gate_out, smoothstepf(0.6f, 1.0f, u) * (g->size.z + 0.02f))));
g->position = vec3(mid.x, g->hinge.y, mid.z);
g->rotation.y = yaw_along(dir); /* (local x along the leaf) */
Object_syncColliders(g);
}
void Object_useToilet(Object *who, Object *toilet) {
if (toilet->dog.mode) return; /* occupied */
who->toilet = toilet;
toilet->dog.mode = 1; /* (furniture: occupied) */
who->sitting = false;
who->position = vec3(toilet->position.x, toilet->position.y + 0.05f, toilet->position.z);
who->velocity = vec3_zero();
Object_syncColliders(who);
Body_pee(&who->body, Object_peeTime(who));
}
#define SEAT_BACK 0.2f /* the hips sit this far behind where the feet are (see body.c) */
#define CHAIR_SPEED 0.9f /* m/s it slides */
#define CHAIR_GRAB 0.9f /* m: within this a free hand is on its backrest while it slides */
void Object_slideChair(Object *chair, float dt) {
if (!chair->tucked_set) return;
chair->chair_out += clampf(chair->chair_goal - chair->chair_out, -CHAIR_SPEED * dt, CHAIR_SPEED * dt);
Vec3 front = chair->colliders[0].axes[2];
front = vec3_normalize(vec3(front.x, 0.0f, front.z));
Vec3 at = vec3_sub(chair->tucked, vec3_scale(front, chair->chair_out));
chair->position = vec3(at.x, chair->position.y, at.z);
Object_syncColliders(chair);
}
/* While a chair slides for who, a free hand holds its backrest. */
static void handOnChair(Object *who, const Object *chair) {
if (chair->type != OBJ_FURNITURE || chair->variant != FURN_CHAIR || fabsf(chair->chair_goal - chair->chair_out) < 1e-3f) return;
Vec3 front = chair->colliders[0].axes[2];
Vec3 grip = vec3_add(chair->position, vec3_add(vec3_scale(front, -chair->size.z * 0.5f), vec3(0.0f, chair->size.y - 0.04f, 0.0f)));
if (vec3_length(vec3_sub(vec3(grip.x, who->position.y, grip.z), who->position)) > CHAIR_GRAB) return;
for (int h = 0; h < 2; h++) {
if (who->hands.item[h] || who->body.hand_active[h]) continue;
who->body.hand_active[h] = true;
who->body.hand_target[h] = grip;
return; /* (one hand does it) */
}
}
/* Everybody (set each frame by the world): who sits where already. */
static Object *const *g_people;
static int g_people_n;
void Object_seePeople(Object *const *objects, int count) { g_people = objects; g_people_n = count; }
#define SEAT_SPACE 0.55f /* m along a bench or a log between two people sitting on it */
/* Where along a bench or log (from its middle, axis along it) somebody can
sit nearest to `want`, clear of everybody already on it; NAN: full. */
static bool clearAt(const Object *who, const Object *seat, Vec3 c, Vec3 axis, float cand) {
for (int i = 0; i < g_people_n; i++) {
const Object *o = g_people[i];
if (o == who || !o->is_character || o->seat != seat || !o->sitting) continue;
if (fabsf(vec3_dot(vec3_sub(o->sit_at, c), axis) - cand) < SEAT_SPACE - 0.06f) return false; /* (a little give: moved up exactly that far) */
}
return true;
}
static float freeAlong(const Object *who, const Object *seat, Vec3 c, Vec3 axis, float want, float half) {
if (seat->type == OBJ_FURNITURE) { /* a bench: its places, evenly along it (sat anywhere, a third didn't fit) - the free one nearest */
int n = (int)(2.0f * half / SEAT_SPACE) + 1;
float gap = n > 1 ? 2.0f * half / (float)(n - 1) : 0.0f, best = NAN;
for (int k = 0; k < n; k++) {
float cand = n > 1 ? -half + gap * (float)k : 0.0f;
if (clearAt(who, seat, c, axis, cand) && (isnan(best) || fabsf(cand - want) < fabsf(best - want))) best = cand;
}
if (!isnan(best)) return best;
}
for (int k = 0; k <= 2 * (int)(half / 0.1f) + 2; k++) {
float cand = want + 0.1f * (float)((k + 1) / 2) * (k % 2 ? 1.0f : -1.0f);
if (cand < -half || cand > half) continue;
if (clearAt(who, seat, c, axis, cand)) return cand;
}
return NAN;
}
/* A bench or a log as a line to sit along: its middle, which way it runs, how far either way. */
static bool seatLine(const Object *seat, Vec3 *c, Vec3 *axis, float *half) {
if (!seat || seat->collider_count == 0) return false;
if (seat->type == OBJ_LOG) {
*c = Object_colliderCenter(seat, 0);
*axis = seat->colliders[0].axes[1];
*half = seat->size.y * 0.35f;
} else if (seat->type == OBJ_FURNITURE && seat->variant == FURN_BENCH) {
*c = seat->position;
Vec3 a = seat->colliders[0].axes[0];
*axis = vec3_normalize(vec3(a.x, 0.0f, a.z));
*half = fmaxf(seat->colliders[0].half_extents.x - 0.28f, 0.0f);
} else return false;
return true;
}
/* Moving up on a bench: everybody on it in order along it, the newcomer at
`want`; spread out till each has SEAT_SPACE, as little moved as can be.
Who won't move (you, the unwilling unless asked, sleepers) stays put.
Fills pos (the newcomer's at index *mine); false: no room even so. */
#define ROOM_MOST 6
static bool spreadOut(const Object *who, const Object *seat, Vec3 c, Vec3 axis, float half, float want, bool asked,
const Object **people, float *pos, float *from, int *n_out, int *mine) {
int n = 0;
for (int i = 0; i < g_people_n && n < ROOM_MOST - 1; i++) {
const Object *o = g_people[i];
if (o == who || !o->is_character || o->seat != seat || !o->sitting) continue;
people[n] = o;
from[n] = vec3_dot(vec3_sub(o->sit_at, c), axis);
n++;
}
people[n] = who;
from[n] = clampf(want, -half, half);
n++;
for (int a = 1; a < n; a++) /* (in order along it) */
for (int b = a; b > 0 && from[b] < from[b - 1]; b--) {
float t = from[b]; from[b] = from[b - 1]; from[b - 1] = t;
const Object *q = people[b]; people[b] = people[b - 1]; people[b - 1] = q;
}
bool fixed[ROOM_MOST];
for (int k = 0; k < n; k++) {
const Object *o = people[k];
pos[k] = from[k];
if (o == who) *mine = k;
fixed[k] = o != who && (o->type == OBJ_PLAYER || o->body.asleep || o->sit_moving ||
(!asked && (o->ai.character.willing < 0.35f || o->ai.annoyed > 0.3f)));
}
for (int pass = 0; pass < 40; pass++) {
for (int k = 1; k < n; k++) {
float gap = pos[k] - pos[k - 1];
if (gap >= SEAT_SPACE) continue;
float d = SEAT_SPACE - gap;
if (fixed[k] && fixed[k - 1]) continue;
if (fixed[k]) pos[k - 1] -= d;
else if (fixed[k - 1]) pos[k] += d;
else { pos[k - 1] -= 0.5f * d; pos[k] += 0.5f * d; }
}
for (int k = 0; k < n; k++) if (!fixed[k]) pos[k] = clampf(pos[k], -half, half);
}
for (int k = 0; k < n; k++) {
if (pos[k] < -half - 0.01f || pos[k] > half + 0.01f) return false;
if (k > 0 && pos[k] - pos[k - 1] < SEAT_SPACE - 0.01f) return false;
}
*n_out = n;
return true;
}
int Object_benchRoom(const Object *who, const Object *seat, Vec3 near, bool asked) {
Vec3 c, axis;
float half;
if (!seatLine(seat, &c, &axis, &half)) return ROOM_NONE;
float want = clampf(vec3_dot(vec3_sub(near, c), axis), -half, half);
if (!isnan(freeAlong(who, seat, c, axis, want, half))) return ROOM_FREE;
const Object *people[ROOM_MOST];
float pos[ROOM_MOST], from[ROOM_MOST];
int n = 0, mine = 0;
return spreadOut(who, seat, c, axis, half, want, asked, people, pos, from, &n, &mine) ? ROOM_SCOOT : ROOM_NONE;
}
bool Object_benchMakeRoom(const Object *who, Object *seat, Vec3 near, bool asked) {
Vec3 c, axis;
float half;
if (!seatLine(seat, &c, &axis, &half)) return false;
float want = clampf(vec3_dot(vec3_sub(near, c), axis), -half, half);
if (!isnan(freeAlong(who, seat, c, axis, want, half))) return true;
const Object *people[ROOM_MOST];
float pos[ROOM_MOST], from[ROOM_MOST];
int n = 0, mine = 0;
if (!spreadOut(who, seat, c, axis, half, want, asked, people, pos, from, &n, &mine)) return false;
for (int k = 0; k < n; k++) {
if (k == mine || fabsf(pos[k] - from[k]) < 0.01f) continue;
Object *o = (Object *)people[k]; /* (one of g_people: the world's own) */
o->sit_at = vec3_add(o->sit_at, vec3_scale(axis, pos[k] - from[k]));
o->sit_scoot = true;
}
return true;
}
#define SCOOT_SPEED 0.35f /* m/s sliding along a bench, sitting */
void Object_scootOnSeat(Object *who, float dt) {
if (!who->sit_scoot) return;
if (!who->sitting || who->sit_moving) { who->sit_scoot = false; return; }
Vec3 to = vec3(who->sit_at.x - who->position.x, 0.0f, who->sit_at.z - who->position.z);
float d = vec3_length(to), step = SCOOT_SPEED * dt;
if (d <= step) { who->position.x = who->sit_at.x; who->position.z = who->sit_at.z; who->sit_scoot = false; }
else { who->position.x += to.x / d * step; who->position.z += to.z / d * step; }
Object_syncColliders(who);
}
static bool seatTaken(const Object *who, const Object *seat) {
for (int i = 0; i < g_people_n; i++)
if (g_people[i] != who && g_people[i]->is_character && g_people[i]->seat == seat && g_people[i]->sitting) return true;
return false;
}
static bool noRoom(Object *who) { who->sitting = false; who->seat = NULL; return false; }
bool Object_sit(Object *who, Object *seat, Vec3 toward) {
who->sitting = true;
who->seat = seat;
who->body.crossing = false; /* (not stepping over it: onto it) */
who->seat_scooted = false;
who->sit_style = SIT_GROUND;
who->body.seat_height = SEAT_HEIGHT;
if (!seat) return true;
if (seat->type == OBJ_LOG) { /* on a log: beside it, facing away from it */
Vec3 c = Object_colliderCenter(seat, 0), axis = seat->colliders[0].axes[1];
Vec3 rel = vec3_sub(who->position, c);
Vec3 across = vec3_normalize(vec3(-axis.z, 0.0f, axis.x));
if (vec3_dot(across, rel) < 0.0f) across = vec3_scale(across, -1.0f);
float want = clampf(vec3_dot(rel, axis), -seat->size.y * 0.35f, seat->size.y * 0.35f);
float along = freeAlong(who, seat, c, axis, want, seat->size.y * 0.35f);
if (isnan(along) && Object_benchMakeRoom(who, seat, who->position, false)) along = freeAlong(who, seat, c, axis, want, seat->size.y * 0.35f); /* (the others move up) */
if (isnan(along)) return noRoom(who); /* (somebody's there already: no sitting on top of them) */
Vec3 at = vec3_add(vec3_add(c, vec3_scale(axis, along)), vec3_scale(across, SEAT_BACK - 0.02f)); /* the bottom on top of it */
who->sit_at = vec3(at.x, who->position.y, at.z);
who->sit_yaw = atan2f(across.z, across.x) + PI_F / 2.0f;
who->sit_dir = vec3_normalize(vec3(across.x, 0.0f, across.z));
who->sit_moving = true; /* (see Object_settleOnSeat) */
who->sit_backing = false;
who->velocity = vec3_zero();
who->sit_style = SIT_SEAT;
who->body.seat_height = fmaxf(seat->size.x, 0.15f);
return true;
}
if (seat->variant == FURN_CHAIR && seatTaken(who, seat)) return noRoom(who);
bool over = false; /* a beer bench: at its table, stepped over it (the others always; you walking up to it, not turned away) */
if (seat->variant == FURN_BENCH && seat->table) {
Vec3 to_seat = vec3(seat->position.x - who->position.x, 0.0f, seat->position.z - who->position.z);
Vec3 to_table = vec3(seat->table->position.x - who->position.x, 0.0f, seat->table->position.z - who->position.z);
bool beyond = vec3_length(to_table) > vec3_length(to_seat); /* (we're on the far side of the bench from the table) */
bool facing = vec3_length(to_seat) < 1e-3f || vec3_dot(Object_facing(who), vec3_normalize(to_seat)) > -0.2f;
if (who->type != OBJ_PLAYER || facing) { toward = seat->table->position; over = beyond; }
}
Vec3 n = seat->colliders[0].axes[2]; /* across the seat */
Vec3 to = vec3_sub(toward, seat->position);
if (seat->variant == FURN_CHAIR || vec3_dot(n, to) < 0.0f) n = seat->variant == FURN_CHAIR ? n : vec3_scale(n, -1.0f);
n = vec3_normalize(vec3(n.x, 0.0f, n.z));
Vec3 base = seat->position;
if (seat->variant == FURN_CHAIR) { /* a chair is pulled out first (it may stand at a table): room in front of it */
if (!seat->tucked_set) { seat->tucked = seat->position; seat->tucked_set = true; }
seat->chair_goal = CHAIR_PULL;
base = vec3_sub(seat->tucked, vec3_scale(n, CHAIR_PULL));
}
/* the bottom in the middle of the seat (a chair: a little back, against the backrest); a bench: where
you are along it, clear of whoever sits on it already */
if (seat->variant != FURN_CHAIR && seat->collider_count > 0) {
Vec3 along_axis = seat->colliders[0].axes[0];
along_axis = vec3_normalize(vec3(along_axis.x, 0.0f, along_axis.z));
float half = fmaxf(seat->colliders[0].half_extents.x - 0.28f, 0.0f);
float want = clampf(vec3_dot(vec3_sub(who->position, seat->position), along_axis), -half, half);
float along = freeAlong(who, seat, seat->position, along_axis, want, half);
if (isnan(along) && Object_benchMakeRoom(who, seat, who->position, false)) along = freeAlong(who, seat, seat->position, along_axis, want, half); /* (the others move up) */
if (isnan(along)) return noRoom(who);
base = vec3_add(base, vec3_scale(along_axis, along));
}
Vec3 at = vec3_add(base, vec3_scale(n, SEAT_BACK + (seat->variant == FURN_CHAIR ? -0.04f : 0.0f)));
who->sit_at = vec3(at.x, who->position.y, at.z);
who->sit_yaw = atan2f(n.z, n.x) + PI_F / 2.0f; /* facing n */
who->sit_dir = n;
who->sit_moving = true;
who->sit_backing = over; /* (over the bench: straight to the spot, no turning round in front of it) */
who->velocity = vec3_zero();
who->sit_style = SIT_SEAT;
who->body.seat_height = SEAT_HEIGHT;
return true;
}
#define SETTLE_SPEED 1.1f /* m/s stepping over to the seat */
#define SETTLE_TURN 4.5f /* rad/s turning round to sit */
#define SETTLE_FRONT 0.3f /* m in front of the seat spot: turned round there */
void Object_leaveSeat(Object *who, float dt) {
Object *seat = who->seat;
if (!seat || who->sitting) return;
if (seat->type == OBJ_FURNITURE && seat->variant == FURN_CHAIR && seat->chair_out < seat->chair_goal - 0.01f) return; /* (out from the table first) */
/* got up: the seat is solid again once you're clear of it */
bool clear = true;
for (int c = 0; c < seat->collider_count && clear; c++) {
const Collider *col = &seat->colliders[c];
Vec3 rel = vec3_sub(who->position, Object_colliderCenter(seat, c));
clear = fabsf(vec3_dot(rel, col->axes[0])) > col->half_extents.x + 0.3f ||
fabsf(vec3_dot(rel, col->axes[2])) > col->half_extents.z + 0.3f;
}
if (clear) {
if (seat->type == OBJ_FURNITURE && seat->variant == FURN_CHAIR) seat->chair_goal = 0.0f; /* pushed in again (see world.c) */
who->seat = NULL;
return;
}
/* ... and standing up you step forward off it */
Vec3 front = vec3_add(who->sit_at, vec3_scale(who->sit_dir, SETTLE_FRONT + 0.1f));
Vec3 to = vec3(front.x - who->position.x, 0.0f, front.z - who->position.z);
float dist = vec3_length(to), step = 0.7f * SETTLE_SPEED * dt;
if (dist > 0.02f && dist < 1.0f && vec3_length(vec3(who->movement.x, 0.0f, who->movement.z)) < 0.1f) {
who->position = vec3_add(who->position, vec3_scale(to, fminf(step, dist) / dist));
Object_syncColliders(who);
} else if (dist <= 0.02f || dist >= 1.0f) {
if (seat->type == OBJ_FURNITURE && seat->variant == FURN_CHAIR) seat->chair_goal = 0.0f;
who->seat = NULL; /* (stood up out of the way) */
}
}
bool Object_settleOnSeat(Object *who, float dt) {
if (!who->sitting || !who->sit_moving) return false;
if (!who->seat) { who->sit_moving = false; return false; }
handOnChair(who, who->seat); /* (the world slides it: Object_slideChair) */
/* first in front of it (feet clear of it), then turned round, a step back */
Vec3 goal = who->sit_backing ? who->sit_at : vec3_add(who->sit_at, vec3_scale(who->sit_dir, SETTLE_FRONT));
Vec3 to = vec3(goal.x - who->position.x, 0.0f, goal.z - who->position.z);
float dist = vec3_length(to), step = SETTLE_SPEED * (who->sit_backing ? 0.5f : 1.0f) * dt;
if (dist > 1.8f) { /* (pushed away meanwhile) */
who->sitting = false;
who->sit_moving = false;
who->seat = NULL;
return false;
}
if (dist > step) { who->position.x += to.x / dist * step; who->position.z += to.z / dist * step; }
else { who->position.x = goal.x; who->position.z = goal.z; }
float turn = remainderf(who->sit_yaw - who->rotation.y, 2.0f * PI_F), most = SETTLE_TURN * dt;
who->rotation.y += clampf(turn, -most, most);
who->velocity.x = who->velocity.z = 0.0f;
who->movement = vec3_zero();
Object_syncColliders(who);
if (dist <= step && fabsf(turn) <= most) {
if (who->sit_backing) who->sit_moving = false; /* there: down onto it */
else who->sit_backing = true;
}
return true;
}
/* Something to sit on: a chair or a bench, else a log lying on the ground. */
static bool isLogSeat(const Object *o) {
return o->type == OBJ_LOG && !o->is_held && !o->lifted && o->mesh.vao && o->size.x >= 0.18f && o->size.y >= 0.5f &&
fabsf(o->colliders[0].axes[1].y) < 0.3f; /* lying, not standing on end */
}
Object *Object_seatNear(Object *const *objects, int count, Vec3 pos, float reach) {
Object *best = NULL;
float best_reach = reach;
for (int i = 0; i < count; i++) { /* chairs and benches first */
Object *o = objects[i];
if (o->type != OBJ_FURNITURE || (o->variant != FURN_CHAIR && o->variant != FURN_BENCH)) continue;
float d = sqrtf((o->position.x - pos.x) * (o->position.x - pos.x) + (o->position.z - pos.z) * (o->position.z - pos.z));
if (d - o->size.x * 0.5f < best_reach) {
best_reach = d - o->size.x * 0.5f;
best = o;
}
}
if (best) return best;
for (int i = 0; i < count; i++) { /* else a log */
Object *o = objects[i];
if (!isLogSeat(o)) continue;
Vec3 c = Object_colliderCenter(o, 0), axis = o->colliders[0].axes[1], rel = vec3_sub(pos, c);
float along = clampf(vec3_dot(rel, axis), -o->size.y * 0.5f, o->size.y * 0.5f);
Vec3 near = vec3_add(c, vec3_scale(axis, along));
float d = sqrtf((near.x - pos.x) * (near.x - pos.x) + (near.z - pos.z) * (near.z - pos.z)) - o->size.x * 0.5f;
if (d < best_reach) {
best_reach = d;
best = o;
}
}
return best;
}
void Object_setDoor(Object *obj, float x) {
if (obj->type != OBJ_HOUSE) return;
BuildingSize b = { .length = obj->size.x, .width = obj->size.z, .wall_height = obj->size.y,
.roof_height = obj->size.z * ROOF_PITCH, .look = (BuildingLook)obj->variant,
.door_set = true, .door_x = x };
Mesh mesh = Shapes_building(b, true);
if (!mesh.vao) return;
Mesh_destroy(&obj->mesh);
obj->mesh = mesh;
}
#define BAG_FULL 24 /* pieces of trash in a full bag */
void Object_fillBag(Object *bag, float added_weight) {
float full = fminf((float)bag->stored / (float)BAG_FULL, 1.0f);
bag->weight += added_weight;
Mesh mesh = Shapes_bag(full);
if (!mesh.vao) return;
Mesh_destroy(&bag->mesh);
bag->mesh = mesh;
bag->size = Shapes_bagSize(full);
bag->pivot = vec3(0.0f, bag->size.y * 0.5f, 0.0f);
bag->collider_count = 0;
addCollider(bag, vec3_zero(), bag->size, vec3_zero());
Object_syncColliders(bag);
}
/* New mesh and colliders for the tree's current trunk and crown. */
static void rebuildTree(Object *obj) {
TreeSize t = treeSize(obj);
Mesh mesh = Shapes_tree(t);
if (!mesh.vao) return;
Mesh_destroy(&obj->mesh);
obj->mesh = mesh;
obj->collider_count = 0;
setupTree(obj, t);
Object_syncColliders(obj);
}
void Object_rebuildTree(Object *obj) {
if (obj->type == OBJ_TREE) rebuildTree(obj);
}
void Object_cutTrunk(Object *obj, float height) {
if (obj->type != OBJ_TREE) return;
height = clampf(height, 0.1f, obj->size.y - 0.5f);
obj->size.y -= height;
obj->position.y += height; /* the tree now starts at the cut */
obj->trunk_cut = true;
rebuildTree(obj);
}
void Object_setBranches(Object *obj, int branches) {
if (obj->type != OBJ_TREE || branches == obj->branches) return;
obj->branches = branches;
if (branches > 0) rebuildTree(obj);
if (!obj->is_static) obj->weight = Object_treeWeight(obj);
}
#define TREE_BRANCH_KG 18.0f /* a layer of branches */
#define TREE_DRAG 0.5f /* dragged along: one end on the ground, about half its weight ... */
#define TREE_SNAG_KG 25.0f /* ... and each layer of branches catches in the ground */
float Object_treeWeight(const Object *obj) {
float r = obj->colliders[0].half_extents.x;
return WOOD_DENSITY * PI_F * r * r * obj->size.y + TREE_BRANCH_KG * (float)obj->branches;
}
float Object_dragWeight(const Object *obj) {
if (obj->type == OBJ_TREE && obj->toppled && !obj->lifted)
return TREE_DRAG * obj->weight + TREE_SNAG_KG * (float)obj->branches;
return obj->weight;
}
void Object_topple(Object *obj, Vec3 dir) {
bool tree = obj->type == OBJ_TREE;
if (!(Object_isBottle(obj) || tree) || obj->toppled || obj->is_held || obj->broken) return;
dir.y = 0.0f;
if (vec3_length(dir) < 1e-4f) return;
dir = vec3_normalize(dir);
/* Tipping by rotation.x tips the top towards -facing (see Object_facing) */
obj->rotation = vec3(0.0f, atan2f(-dir.z, -dir.x) + PI_F / 2.0f, 0.0f);
obj->toppled = true;
obj->topple = 0.0f;
obj->fall_dir = dir;
obj->fall_base_y = obj->position.y;
obj->fall_origin = obj->position;
obj->fall_ground_y = obj->position.y; /* (the world says where the ground is: see Forestry) */
obj->fall_slide = 0.0f;
Object_syncColliders(obj);
}
void Object_makeLog(Object *obj, Vec3 center, Vec3 axis, float length, float diameter) {
Mesh log = Shapes_log(diameter, length);
if (!log.vao) return;
Mesh_destroy(&obj->mesh);
obj->mesh = log;
obj->type = OBJ_LOG;
obj->is_static = false;
obj->toppled = false;
obj->topple = 0.0f;
obj->work = 0.0f;
obj->stump = NULL;
obj->pivot = vec3(0.0f, length * 0.5f, 0.0f);
obj->size = vec3(diameter, length, 0.0f);
obj->weight = WOOD_DENSITY * PI_F * diameter * diameter * 0.25f * length;
obj->collider_count = 0;
addCollider(obj, vec3_zero(), vec3(diameter, length, diameter), vec3_zero());
/* Local Y along the axis; local Z any direction across it */
axis = vec3_normalize(axis);
Vec3 across = fabsf(axis.y) < 0.9f ? vec3(0, 1, 0) : vec3(1, 0, 0);
across = vec3_normalize(vec3_sub(across, vec3_scale(axis, vec3_dot(across, axis))));
obj->rotation = euler_yxz_from_axes(axis, across);
obj->position = vec3_sub(center, obj->pivot);
obj->velocity = vec3_zero();
obj->angular_velocity = vec3_zero();
Object_syncColliders(obj);
}
void Object_makeFirewood(Object *obj, Vec3 center, Vec3 axis, Vec3 up, float length, float diameter, int parts) {
Mesh_destroy(&obj->mesh);
obj->type = OBJ_FIREWOOD;
obj->is_static = false;
obj->toppled = false;
obj->topple = 0.0f;
obj->work = 0.0f;
obj->stump = NULL;
setupFirewood(obj, diameter, length, parts);
if (!obj->mesh.vao) return;
axis = vec3_normalize(axis);
up = vec3_normalize(vec3_sub(up, vec3_scale(axis, vec3_dot(up, axis))));
obj->rotation = euler_yxz_from_axes(axis, up);
obj->position = vec3_sub(center, obj->pivot);
obj->velocity = vec3_zero();
obj->angular_velocity = vec3_zero();
Object_syncColliders(obj);
}
/* Where the k-th piece of firewood lies on a woodpile: rows of quarters lying
across the sleepers, layer on layer; odd layers have one piece less and sit
in the gaps of the layer below. */
static Vec3 pileSlot(const Object *pile, float r, int k, int *layer_out) {
const float width = r + 0.01f;
const Vec3 along = pile->colliders[0].axes[0], up = vec3(0.0f, 1.0f, 0.0f);
int per_layer = (int)(pile->size.x / width);
if (per_layer < 1) per_layer = 1;
int col = k, layer = 0;
while (col >= per_layer - (layer % 2) && per_layer > 1) {
col -= per_layer - (layer % 2);
layer++;
}
int in_layer = per_layer > 1 ? per_layer - (layer % 2) : 1;
if (per_layer == 1) { layer = col; col = 0; }
float x = ((float)col - (float)(in_layer - 1) * 0.5f) * width;
if (layer_out) *layer_out = layer;
Vec3 top = vec3_add(Object_colliderCenter(pile, 0), vec3(0.0f, WOODPILE_BASE * 0.5f, 0.0f));
return vec3_add(vec3_add(top, vec3_scale(along, x)), vec3_scale(up, (float)layer * (r + 0.005f) + r * 0.5f + 0.003f));
}
/* Firewood lying on the pile (not carried off). */
bool Object_onPile(const Object *pile, const Object *o) {
if (o->type != OBJ_FIREWOOD || !o->stored || o->is_held || !o->mesh.vao) return false;
Vec3 rel = vec3_sub(Object_colliderCenter(o, 0), Object_colliderCenter(pile, 0));
return fabsf(vec3_dot(rel, pile->colliders[0].axes[0])) < pile->size.x * 0.5f + 0.15f &&
fabsf(vec3_dot(rel, pile->colliders[0].axes[2])) < pile->size.z * 0.5f + 0.5f && rel.y > -0.2f && rel.y < 2.5f;
}
int Object_pileCount(const Object *pile, Object *const *objects, int count) {
int n = 0;
for (int i = 0; i < count; i++)
if (Object_onPile(pile, objects[i])) n++;
return n;
}
/* Small dice from a position and a number (no shared state). */
static float pileDice(Vec3 p, int k) {
unsigned h = (unsigned)(int)(p.x * 331.0f) * 73856093u ^ (unsigned)(int)(p.z * 257.0f) * 19349663u ^ (unsigned)k * 83492791u;
h ^= h >> 13; h *= 0x5bd1e995u; h ^= h >> 15;
return (float)(h >> 8) / 16777216.0f;
}
#define PILE_SLIP_FROM 3 /* a loose pile this many layers high may come down */
#define PILE_SORT_CARE 0.7f /* this careful: sorted by length, the front even */
#define PILE_PIECE 0.6f /* the usual length of a piece */
#define PILE_SAME 0.04f /* (this close to it counts as usual) */
int Object_stackFirewood(Object *pile, Object *piece, Object *const *objects, int count, float care) {
/* The lowest place that's free (pieces taken off leave gaps lower down). Careful
people sort by length: long pieces from one end of the layer, short ones from the other */
const float r = piece->size.x * 0.5f;
const bool sorts = care >= PILE_SORT_CARE;
const bool short_one = piece->size.y < PILE_PIECE - PILE_SAME;
int layer = 0, first_layer = -1;
Vec3 center = pileSlot(pile, r, 0, &layer), chosen = center;
int chosen_layer = 0;
for (int k = 0; k < 400; k++) {
center = pileSlot(pile, r, k, &layer);
if (first_layer >= 0 && layer != first_layer) break; /* (sorting: that layer's free places looked at) */
bool taken = false;
for (int i = 0; i < count && !taken; i++) {
const Object *o = objects[i];
if (o == piece || !Object_onPile(pile, o)) continue;
Vec3 d = vec3_sub(Object_colliderCenter(o, 0), center);
if (fabsf(d.y) < r * 0.7f && fabsf(vec3_dot(d, pile->colliders[0].axes[0])) < r * 0.7f) taken = true;
}
if (taken) continue;
if (first_layer < 0) { chosen = center; chosen_layer = layer; first_layer = layer; }
else if (short_one) { chosen = center; chosen_layer = layer; } /* (the last free one: the other end) */
if (!sorts || !short_one) break;
}
center = chosen;
layer = chosen_layer;
Vec3 across = pile->colliders[0].axes[2], along = pile->colliders[0].axes[0];
if (sorts) /* (and pushed in so the front is even, whatever the length) */
center = vec3_add(center, vec3_scale(across, (PILE_PIECE - piece->size.y) * 0.5f));
const Vec3 up = vec3(0.0f, 1.0f, 0.0f);
/* Careless: not quite in its place - pushed in more or less, a bit to the side, askew */
const float loose = 1.0f - clampf(care, 0.0f, 1.0f);
const int n = pile->stored;
if (loose > 0.05f) {
center = vec3_add(center, vec3_add(vec3_scale(across, (pileDice(center, n) - 0.5f) * 0.3f * loose),
vec3_scale(along, (pileDice(center, n + 7) - 0.5f) * 0.5f * r * loose)));
float twist = (pileDice(center, n + 13) - 0.5f) * 0.5f * loose;
across = vec3_add(vec3_scale(across, cosf(twist)), vec3_scale(along, sinf(twist)));
center.y += 0.01f * loose; /* (resting on the uneven layer below) */
}
Object_makeFirewood(piece, center, across, up, piece->size.y, piece->size.x, 4);
piece->stored = 1;
pile->stored = Object_pileCount(pile, objects, count);
/* A loose pile getting high: now and then the top comes down */
int slid = 0;
if (layer >= PILE_SLIP_FROM && pileDice(center, n + 29) < (loose - 0.45f) * 0.25f * (float)(layer - PILE_SLIP_FROM + 1)) {
float side = pileDice(center, n + 31) < 0.5f ? -1.0f : 1.0f;
const float top = center.y - 1.5f * r;
for (int i = 0; i < count; i++) {
Object *o = objects[i];
if (!Object_onPile(pile, o) || Object_colliderCenter(o, 0).y < top) continue;
Vec3 push = vec3_add(vec3_scale(pile->colliders[0].axes[2], side * (0.9f + 0.6f * pileDice(o->position, i))),
vec3_scale(along, (pileDice(o->position, i + 3) - 0.5f) * 0.8f));
o->velocity = vec3_add(push, vec3(0.0f, 0.4f, 0.0f));
o->angular_velocity = vec3_scale(along, side * 3.0f);
o->stored = 0;
o->rest_time = 0.0f;
slid++;
}
pile->stored = Object_pileCount(pile, objects, count);
}
return slid;
}
void Object_markCut(Object *o, Vec3 point, float progress) {
o->mark = progress;
if (progress <= 0.0f || o->collider_count == 0) { o->mark = 0.0f; return; }
/* in the object's own frame (local Y along the wood), outwards from the axis */
Mat4 r = mat4_rotate_yxz(o->rotation);
Vec3 ax = mat4_mul_dir(r, vec3(1, 0, 0)), ay = mat4_mul_dir(r, vec3(0, 1, 0)), az = mat4_mul_dir(r, vec3(0, 0, 1));
Vec3 v = vec3_sub(point, vec3_add(o->position, o->pivot));
Vec3 local = vec3(vec3_dot(v, ax), vec3_dot(v, ay), vec3_dot(v, az));
Vec3 out = vec3(local.x, 0.0f, local.z);
o->mark_at = local;
o->mark_out = vec3_length(out) > 1e-3f ? vec3_normalize(out) : vec3(1.0f, 0.0f, 0.0f);
}
float Object_bottom(const Object *obj) {
if (obj->collider_count == 0) return obj->position.y;
const Collider *c = &obj->colliders[0];
float extent = fabsf(c->axes[0].y) * c->half_extents.x + fabsf(c->axes[1].y) * c->half_extents.y +
fabsf(c->axes[2].y) * c->half_extents.z;
return Object_colliderCenter(obj, 0).y - extent;
}
static const char *bottleName(const Object *b) {
static const char *const DRINK[] = { "Water", "Cola", "Orange soda", "Mate" };
return b->type == OBJ_VODKA ? "Vodka" : b->type == OBJ_WATER ? DRINK[b->variant & 3] : "Beer";
}
void Object_showUmbrella(Object *u) {
static const Vec3 COLORS[] = { { 0.15f, 0.20f, 0.42f }, { 0.70f, 0.14f, 0.16f }, { 0.20f, 0.32f, 0.22f } };
int want = u->umbrella_flip > 0.0f || u->umbrella_ruined ? UMBRELLA_INSIDE_OUT : u->umbrella_open ? UMBRELLA_OPEN : UMBRELLA_FOLDED;
if (u->type != OBJ_UMBRELLA || u->shown == want) return;
Mesh_destroy(&u->mesh);
u->mesh = Shapes_umbrella(UMBRELLA_LENGTH, UMBRELLA_RADIUS, want, COLORS[u->variant % 3]);
u->shown = want;
}
void Object_setDrink(Object *b, int drink) {
static const BottleStyle STYLE[] = { BOTTLE_WATER, BOTTLE_COLA, BOTTLE_ORANGE, BOTTLE_MATE };
if (b->type != OBJ_WATER) return;
drink &= 3;
b->variant = drink;
Mesh_destroy(&b->mesh);
b->mesh = Shapes_bottle(2.0f * b->colliders[0].half_extents.x, 2.0f * b->colliders[0].half_extents.y, STYLE[drink]);
b->quench = drink == 0 ? 0.2f : 0.15f;
b->refresh = drink == 0 ? 0.15f : drink == 3 ? 0.12f : 0.1f; /* (sugar, or the mate's kick) */
b->nourish = drink == 1 || drink == 2 ? 0.02f : 0.0f;
b->wake = drink == 3 ? 0.04f : drink == 1 ? 0.01f : 0.0f; /* (caffeine: less sleepy) */
}
bool Object_isHard(const Object *o) {
switch (o->type) {
case OBJ_STONE: case OBJ_BOX_STATIC: case OBJ_CYLINDER_STATIC: case OBJ_HOUSE: case OBJ_BARN: case OBJ_STUMP: case OBJ_WALL:
case OBJ_FIREPLACE: case OBJ_FURNITURE: case OBJ_BIN: case OBJ_SUPPLY: case OBJ_LADDER:
case OBJ_AXE: case OBJ_CHAINSAW: case OBJ_BOTTLE: case OBJ_VODKA: case OBJ_WATER: case OBJ_CAN:
return true;
default:
return false; /* grass, wood, people, bushes, bags: it just lands */
}
}
void Object_describe(const Object *obj, char *out, size_t size) {
const int kg = (int)(obj->weight + 0.5f);
switch (obj->type) {
case OBJ_BOTTLE:
case OBJ_VODKA:
case OBJ_WATER:
if (obj->sips > 0) snprintf(out, size, "%s - %d sip%s left", bottleName(obj), obj->sips, obj->sips == 1 ? "" : "s");
else snprintf(out, size, "%s - empty (back into a crate with it)", bottleName(obj));
break;
case OBJ_APPLE:
case OBJ_BREAD:
case OBJ_SAUSAGE: {
const char *name = obj->type == OBJ_APPLE ? "apple" : obj->type == OBJ_BREAD ? "bread" : "sausage";
const char *state = obj->burnt ? "Burnt " : obj->grilled ? "Grilled " : "";
char what[48];
snprintf(what, sizeof what, "%s%s", state, name);
if (!*state) what[0] = (char)(what[0] - 'a' + 'A');
char how[64] = "";
if (obj->burnt) snprintf(how, sizeof how, " - charcoal");
else if (obj->grill >= FOOD_DARK) snprintf(how, sizeof how, " - getting black, take it off!");
else if (obj->grilled && obj->on_grill) snprintf(how, sizeof how, " - done, take it off");
else if (obj->grill > 0.0f && !obj->grilled) snprintf(how, sizeof how, " - %d%% done", (int)(100.0f * obj->grill / FOOD_DONE));
else if (obj->type == OBJ_SAUSAGE && !obj->grilled) snprintf(how, sizeof how, " - grill it: click the fire with it");
snprintf(out, size, "%s - %d bite%s left%s", what, obj->sips, obj->sips == 1 ? "" : "s", how);
break;
}
case OBJ_TRASH: {
static const char *const WHAT[] = { [PALETTE_STANDARD] = "Plastic bottle", [PALETTE_GROUND] = "Broken board",
[PALETTE_MODERN] = "Empty can", [PALETTE_WARM] = "Paper" };
snprintf(out, size, "%s - trash", obj->name[0] ? obj->name : WHAT[obj->variant & 3]);
break;
}
case OBJ_BAG:
snprintf(out, size, obj->stored >= BAG_FULL ? "Garbage bag - full, take it to the container"
: obj->stored > 0 ? "Garbage bag - %d pieces in it" : "Garbage bag - click on trash to put it in", obj->stored);
break;
case OBJ_BIN: snprintf(out, size, "Waste container - throw trash and bags in"); break;
case OBJ_FURNITURE: {
static const char *const WHAT[FURN_COUNT] = {
[FURN_WORKBENCH] = "Workbench", [FURN_TOOLWALL] = "Tool wall", [FURN_SHELF] = "Shelf",
[FURN_STOVE] = "Stove - hold food over it to cook it", [FURN_COUNTER] = "Kitchen sink - click to wash",
[FURN_TABLE] = "Table", [FURN_CHAIR] = "Chair - C to sit down", [FURN_BENCH] = "Log bench - C to sit down",
[FURN_CAR] = "An old car, up for repairs", [FURN_TIRES] = "Old tyres",
[FURN_TOILET] = "Toilet - P to use it", [FURN_TAP] = "Tap - the garden hose; fill the watering can here",
[FURN_BAR] = "The bar",
};
if (obj->variant == FURN_ASHTRAY) { snprintf(out, size, "Ashtray (an old tin) - %d butts in it", obj->stored); break; }
if (obj->variant == FURN_SMOKES) { snprintf(out, size, "Cigarette machine - click: a pack of %d", SMOKES_PACKS); break; }
if (obj->variant == FURN_BENCH && obj->table) { snprintf(out, size, "Beer bench - C: over it and sit at the table (turned away: your back to it)"); break; }
snprintf(out, size, "%s", obj->variant >= 0 && obj->variant < FURN_COUNT && WHAT[obj->variant] ? WHAT[obj->variant] : "Furniture");
break;
}
case OBJ_HOSE: snprintf(out, size, "Garden hose - left click (hold) to spray: washes away puddles and dirt"); break;
case OBJ_LADDER:
if (obj->variant == 2) /* (step ladder) */
snprintf(out, size, obj->leaning ? "Step ladder - left click: climb (W / S), E / Q: fold it up"
: "Step ladder - left click: stand it up in front of you (at a wall: with its back to it)");
else if (obj->variant == 1) /* (folding) */
snprintf(out, size, obj->leaning ? "Folding ladder - left click: climb (W / S), E / Q: take it down"
: obj->extend_goal > 0.5f ? "Folding ladder (unfolded) - left click where it should lean, look down: fold it"
: "Folding ladder (folded) - left click at a wall: unfold it and lean it there");
else
snprintf(out, size, obj->leaning ? "Ladder - left click: climb (W / S), E / Q: take it down"
: obj->extend_goal > 0.5f ? "Ladder (slid out) - left click at a wall: lean it there, else slide it together"
: "Ladder (slid together) - left click: slide it out, then lean it against something");
break;
case OBJ_STONE: snprintf(out, size, kg >= 1 ? "A stone - %d kg" : "A pebble", kg); break;
case OBJ_CAR:
if (obj->variant == 1) { snprintf(out, size, "The ambulance"); break; }
if (!obj->load || obj->load->n == 0) snprintf(out, size, "Volkram's car - the boot is empty");
else if (!obj->boot_open) snprintf(out, size, "Volkram's car - full of your things");
else snprintf(out, size, "Volkram's car - left click at the boot: take out %s", Car_nextName(obj));
break;
case OBJ_CRATE:
{ /* its own bottles, full and empty; and what doesn't belong in it (right click takes that out) */
int full = 0, empty = 0, wrong = 0;
for (int k = 0; k < CRATE_SLOTS; k++) {
int kind = CRATE_KIND(obj->crate_slot[k]);
if (!kind) continue;
if (!Crate_belongs(obj, kind)) wrong++;
else if (CRATE_SIPS(obj->crate_slot[k]) > 0) full++;
else empty++;
}
char extra[64] = "";
if (wrong > 0) snprintf(extra, sizeof extra, ", %d not %s%s", wrong, Crate_name(obj->variant), obj->is_held ? "" : " (right click: out)");
if (empty > 0) snprintf(out, size, "Crate of %s - %d full, %d empt%s%s%s", Crate_name(obj->variant), full, empty, empty == 1 ? "y" : "ies", extra,
obj->stored > obj->crate_empties && !obj->is_held ? " (left click: take one)" : "");
else snprintf(out, size, "Crate of %s - %d bottle%s left%s%s", Crate_name(obj->variant), full, full == 1 ? "" : "s", extra,
obj->stored > 0 && !obj->is_held ? " (left click: take one)" : "");
}
break;
case OBJ_TENT: {
static const char *const STAGE[] = { "spread out - left click: put the poles in", "the poles going in - keep clicking",
"the cover going over it - keep clicking", "pegging it down - keep clicking", "up" };
const char *whose = obj->owner && obj->owner->type == OBJ_PLAYER ? "Your" : NULL;
if (whose) snprintf(out, size, "Your tent - %s", STAGE[obj->tent_stage % 5]);
else snprintf(out, size, "%s's tent - %s", obj->owner ? obj->owner->name : "Somebody", STAGE[obj->tent_stage % 5]);
break;
}
case OBJ_STAGE: {
int pct = (int)(Stage_part(obj) * 100.0f);
if (obj->build_stage >= BUILD_DONE) snprintf(out, size, "The %s stage - built!", Cast_projectName((Project)obj->variant));
else snprintf(out, size, "The %s stage - %s %d%% (hold left click to work on it)", Cast_projectName((Project)obj->variant), Stage_stepName(obj->build_stage), pct);
break;
}
case OBJ_MATERIAL: {
static const char *const WHAT[MATERIAL_KINDS] = { "A bundle of boards (8, cut to length, for a deck or a roof) - both hands", "A box of nails (for a deck)",
"A box of screws (for a roof)", "A bucket of paint" };
snprintf(out, size, "%s - %d kg", WHAT[obj->variant % MATERIAL_KINDS], kg);
break;
}
case OBJ_SCYTHE: snprintf(out, size, "Scythe - for the Kartoffelacker's grass (in the field: hold left click and walk)"); break;
case OBJ_ROPE: snprintf(out, size, "Throw line - up over a branch of the big tree, as high as you can (T: hold to wind up, look up, let go)"); break;
case OBJ_SNAIL:
snprintf(out, size, obj->variant ? "A vineyard snail - safe here" : "A vineyard snail (protected) - left click: into your bucket, let go at the wood by the Kartoffelacker");
break;
case OBJ_SCULPTURE: {
static const char *const WHAT[] = { "an owl", "a bear", "a mushroom man" };
snprintf(out, size, "Kerry's wood art - %s carved with the chainsaw", WHAT[obj->variant % 3]);
break;
}
case OBJ_HAT: {
const char *what = obj->variant == 2 ? "beanie" : "cap";
if (obj->owner && obj->owner->type == OBJ_PLAYER) snprintf(out, size, "Your %s - pick it up to put it back on", what);
else if (obj->owner) snprintf(out, size, "%s's %s (blown off)", obj->owner->name, what);
else snprintf(out, size, "A %s", what);
break;
}
case OBJ_UMBRELLA:
snprintf(out, size, obj->umbrella_ruined ? "A broken umbrella - blown inside out, the ribs bent" : obj->umbrella_flip > 0.0f ? "Umbrella - blown inside out!" :
obj->umbrella_open ? "Umbrella - open" : "Umbrella (carried, it opens in the rain)");
break;
case OBJ_GEAR: {
static const char *const WHAT[] = { "sleeping bag", "sleeping mat", "tent", "tent (the big one)" };
if (obj->owner && obj->owner->type == OBJ_PLAYER) snprintf(out, size, "Your %s", WHAT[obj->variant & 3]);
else snprintf(out, size, "%s's %s", obj->owner ? obj->owner->name : "Somebody", WHAT[obj->variant & 3]);
break;
}
case OBJ_CLOTHES:
if (obj->cloth_of && obj->cloth_of->type == OBJ_PLAYER) snprintf(out, size, "Your pullover - click: take it");
else snprintf(out, size, "%s's pullover", obj->cloth_of ? obj->cloth_of->name : "Somebody");
break;
case OBJ_TOY: snprintf(out, size, obj->variant == 0 ? "Alice's ball - throw it (T), she brings it back" : "Alice's rope toy - throw it (T), she loves to chew it"); break;
case OBJ_CAN:
if (obj->water > 0.05f) snprintf(out, size, "Watering can - %.0f l, left click (hold) to pour", obj->water);
else snprintf(out, size, "Watering can - empty, fill it at the tap");
break;
case OBJ_FIREPLACE:
if (obj->burning) snprintf(out, size, "Fire - %d kg of wood left", (int)(obj->fuel + 0.5f));
else if (obj->fuel > 0.0f) snprintf(out, size, "Fireplace - %d kg of wood, click to light it", (int)(obj->fuel + 0.5f));
else snprintf(out, size, "Fireplace - put firewood or branches in");
break;
case OBJ_AXE: snprintf(out, size, "Axe - fells, cuts and splits wood"); break;
case OBJ_SHEARS: snprintf(out, size, "Pruning shears - cut branches off (or a burnt bit out of the hose)"); break;
case OBJ_TAPE: snprintf(out, size, "Duct tape - left click at a melted bit of the hose: tape it up"); break;
case OBJ_CHAINSAW: snprintf(out, size, "Chainsaw - fells and cuts fast, %d kg", kg); break;
case OBJ_LOG:
snprintf(out, size, "Log %.1f m, %d kg%s", obj->size.y, kg,
obj->size.y <= 0.75f ? " - split it with the axe" : "");
break;
case OBJ_FIREWOOD:
snprintf(out, size, "Firewood, %d kg%s", kg, obj->size.z < 4.0f ? " - can be split once more" : "");
break;
case OBJ_BRANCH: snprintf(out, size, "Branch"); break;
case OBJ_WOODPILE: snprintf(out, size, "Woodpile - %d pieces of firewood", obj->stored); break;
case OBJ_TREE:
if (obj->variant == 2 && !obj->toppled) snprintf(out, size, "The big old tree - half dead, top broken off, leaning to the road. It must come down before it falls: Enrico's job");
else if (obj->variant == 2) snprintf(out, size, "The big old tree, down - %d branch%s left", obj->branches, obj->branches == 1 ? "" : "es");
else if (!obj->toppled && obj->variant) snprintf(out, size, "Apple tree - pick an apple");
else if (!obj->toppled && obj->keep) snprintf(out, size, "Tree - this one stays (Kurti's rule)");
else if (!obj->toppled) snprintf(out, size, "Tree - fell it with the axe or the chainsaw");
else snprintf(out, size, "Fallen tree - %d branch%s left", obj->branches, obj->branches == 1 ? "" : "es");
break;
case OBJ_STUMP:
if (obj->work > 0.0f) snprintf(out, size, "Tree stump - %d%% chopped out", (int)(100.0f * fminf(obj->work / STUMP_WORK, 1.0f)));
else snprintf(out, size, "Tree stump - chop it out with the axe or the chainsaw");
break;
case OBJ_SUPPLY:
if (obj->stock < 0) { snprintf(out, size, obj->dispenses == OBJ_APPLE ? "Apple crate - empty" : "Cool box - empty"); break; }
if (obj->stock > 0) {
snprintf(out, size, obj->dispenses == OBJ_APPLE ? "Food crate - %d portions left (take an apple)" : "Cool box - %d sausages left (take one)", obj->stock);
break;
}
snprintf(out, size, obj->dispenses == OBJ_APPLE ? "Apple crate - take an apple"
: obj->dispenses == OBJ_SAUSAGE ? "Cool box - take a sausage"
: obj->dispenses == OBJ_BAG ? "Garbage bags - take one"
: obj->dispenses == OBJ_URINELLA ? "Urinella box - fill your pocket (one-time use)" : "Water barrel - take a bottle");
break;
case OBJ_BOX:
case OBJ_CYLINDER:
snprintf(out, size, "%s, %d kg", obj->type == OBJ_BOX ? "Crate" : "Drum", kg);
break;
case OBJ_WALL:
snprintf(out, size, "%s", obj->name[0] ? obj->name : "Wall");
break;
default:
snprintf(out, size, "%d kg", kg);
break;
}
}
void Object_advanceTopple(Object *obj, float dt) {
if (!obj->toppled || obj->topple >= 1.0f) return;
bool tree = obj->type == OBJ_TREE;
obj->topple = fminf(1.0f, obj->topple + dt / (tree ? TREE_FALL_TIME : BOTTLE_FALL_TIME));
float t = obj->topple;
obj->rotation.x = -(PI_F / 2.0f) * t * t; /* speeds up as it falls */
if (tree) { /* the butt slides off the stump as it comes down, the trunk rests on the ground, not in it */
float off = smoothstepf(0.55f, 1.0f, t);
Vec3 at = vec3_add(obj->fall_origin, vec3_scale(obj->fall_dir, obj->fall_slide * off));
obj->position = vec3(at.x, lerpf(obj->fall_base_y, obj->fall_ground_y, off) + obj->colliders[0].half_extents.x * smoothstepf(0.8f, 1.0f, t), at.z);
}
Object_syncColliders(obj);
}
void Object_useUp(Object *obj) {
Mesh_destroy(&obj->mesh);
obj->broken = true;
obj->is_static = true;
obj->is_held = false;
obj->collider_count = 0;
obj->sips = 0;
}
void Object_shatter(Object *obj, float ground_y) {
if (!Object_isBottle(obj) || obj->broken) return;
float diameter = 2.0f * obj->colliders[0].half_extents.x;
unsigned seed = (unsigned)(obj->position.x * 997.0f) ^ ((unsigned)(obj->position.z * 991.0f) << 12);
Mesh shards = Shapes_shards(diameter, seed);
if (!shards.vao) return;
Mesh_destroy(&obj->mesh);
obj->mesh = shards;
obj->broken = true;
obj->is_static = true; /* no physics, can't be picked up */
obj->collider_count = 0;
obj->sips = 0;
obj->thrown_by = NULL;
obj->pivot = vec3_zero();
obj->rotation = vec3(0.0f, obj->rotation.y, 0.0f);
obj->position.y = ground_y;
obj->velocity = vec3_zero();
}