#include "world/object.h" #include "world/stage.h" #include "world/car.h" #include #include #include #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(); }