diff --git a/src/cache.h b/src/cache.h index ef131fe..0d0aa89 100644 --- a/src/cache.h +++ b/src/cache.h @@ -741,46 +741,144 @@ struct WaterCache { #undef DETAIL }; -/* -struct LightCache { +struct ZoneCache { struct Item { - int room; - int index; - float intensity; - } items[MAX_CACHED_LIGHTS]; + uint16 zone; + uint16 count; + uint16 *zones; + uint16 *boxes; + Item *next; - void updateLightCache(const TR::Level &level, const vec3 &pos, int room) { - // update intensity - for (int i = 0; i < MAX_CACHED_LIGHTS; i++) { - Item &item = items[i]; - if (c.intensity < 0.0f) continue; - TR::Room::Light &light = level.rooms[c.room].lights[i]; - c.intensity = max(0.0f, 1.0f - (pos - vec3(float(light.x), float(light.y), float(light.z))).length2() / ((float)light.radius * (float)light.radius)); + Item(uint16 zone, uint16 count, uint16 *zones, uint16 *boxes, Item *next) : + zone(zone), count(count), zones(zones), boxes(boxes), next(next) {} + + ~Item() { + delete[] boxes; + delete next; + } + } *items; + + IGame *game; + // dummy arrays for path search + uint16 *nodes; + uint16 *parents; + uint16 *weights; + + ZoneCache(IGame *game) : items(NULL), game(game) { + TR::Level *level = game->getLevel(); + nodes = new uint16[level->boxesCount * 3]; + parents = nodes + level->boxesCount; + weights = nodes + level->boxesCount * 2; + } + + ~ZoneCache() { + delete items; + delete[] nodes; + } + + Item *getBoxes(uint16 zone, uint16 *zones) { + Item *item = items; + while (item) { + if (item->zone == zone && item->zones == zones) + return item; + item = item->next; } - // check for new lights - int index = getLightIndex(pos, room); + int count = 0; + TR::Level *level = game->getLevel(); + for (int i = 0; i < level->boxesCount; i++) + if (zones[i] == zone) + nodes[count++] = i; - if (index >= 0 && (items[0].room != room || items[0].index != index)) [ - TR::Room::Light &light = level.rooms[room].lights[index]; - float intensity = max(0.0f, 1.0f - (lara->pos - vec3(float(light.x), float(light.y), float(light.z))).length2() / ((float)light.radius * (float)light.radius)); + ASSERT(count > 0); + uint16 *boxes = new uint16[count]; + memcpy(boxes, nodes, sizeof(uint16) * count); - int i = 0; - while (i < MAX_CACHED_LIGHTS && lightCache[i].intensity > intensity) // get sorted place - i++; - if (i < MAX_CACHED_LIGHTS) { // insert light - for (int j = MAX_CACHED_LIGHTS - 1; j > i; j--) - lightCache[j] = lightCache[j - 1]; - lightCache[i].room = room; - lightCache[i].index = index; - lightCache[i].intensity = intensity; + return items = new Item(zone, count, zones, boxes, items); + } + + uint16 findPath(int ascend, int descend, int boxStart, int boxEnd, uint16 *zones, uint16 **boxes) { + if (boxStart == 0xFFFF || boxEnd == 0xFFFF) + return 0; + + TR::Level *level = game->getLevel(); + memset(parents, 0xFF, sizeof(uint16) * level->boxesCount); // fill parents by 0xFFFF + memset(weights, 0x00, sizeof(uint16) * level->boxesCount); // zeroes weights + + uint16 count = 0; + nodes[count++] = boxEnd; + + uint16 zone = zones[boxStart]; + + if (zone != zones[boxEnd]) + return 0; + + TR::Box &b = level->boxes[boxStart]; + + int sx = (b.minX + b.maxX) >> 11; // box center / 1024 + int sz = (b.minZ + b.maxZ) >> 11; + + while (count) { + // get min weight + int minI = 0; + int minW = weights[nodes[minI]]; + for (int i = 1; i < count; i++) + if (weights[nodes[i]] < minW) { + minI = i; + minW = weights[nodes[i]]; + } + int cur = nodes[minI]; + + // peek min weight item from array + count--; + for (int i = minI; i < count; i++) + nodes[i] = nodes[i + 1]; + + // check for end of path + if (cur == boxStart) { + count = 0; + while (cur != boxEnd) { + nodes[count++] = cur; + cur = parents[cur]; + } + nodes[count++] = cur; + *boxes = nodes; + return count; } + // add overlap boxes + TR::Box &b = game->getLevel()->boxes[cur]; + TR::Overlap *overlap = &level->overlaps[b.overlap.index]; + + do { + uint16 index = overlap->boxIndex; + // unvisited yet + if (parents[index] != 0xFFFF) + continue; + // has same zone + if (zones[index] != zone) + continue; + // check for height difference + int d = level->boxes[index].floor - b.floor; + if (d > ascend || d < descend) + continue; + + int dx = sx - ((b.minX + b.maxX) >> 11); + int dz = sz - ((b.minZ + b.maxZ) >> 11); + int w = abs(dx) + abs(dz); + + ASSERT(count < level->boxesCount); + nodes[count++] = index; + parents[index] = cur; + weights[index] = weights[cur] + w; + + } while (!(overlap++)->end); } + + return 0; } }; -*/ #undef UNDERWATER_COLOR diff --git a/src/character.h b/src/character.h index 593c317..0c6cf88 100644 --- a/src/character.h +++ b/src/character.h @@ -46,11 +46,18 @@ struct Character : Controller { updateZone(); } - void updateZone() { - TR::Level *level = game->getLevel(); + bool updateZone() { int dx, dz; - box = level->getSector(getRoomIndex(), int(pos.x), int(pos.z), dx, dz).boxIndex; - zone = flying ? level->zones[0].fly[box] : level->zones[0].ground1[box]; + TR::Room::Sector &s = level->getSector(getRoomIndex(), int(pos.x), int(pos.z), dx, dz); + if (s.boxIndex == 0xFFFF) + return false; + box = s.boxIndex; + zone = getZones()[box]; + return true; + } + + uint16* getZones() { + return flying ? level->zones[0].fly : level->zones[0].ground1; } void rotateY(float delta) { @@ -130,6 +137,11 @@ struct Character : Controller { animation.setState(getStateDefault()); } + virtual void updateTilt(float value, float tiltSpeed, float tiltMax) { + value = clamp(value, -tiltMax, +tiltMax); + decrease(value - angle.z, angle.z, tiltSpeed); + } + virtual void updateTilt(bool active, float tiltSpeed, float tiltMax) { // calculate turning tilt if (active && (input & (LEFT | RIGHT)) && (tilt == 0.0f || (tilt < 0.0f && (input & LEFT)) || (tilt > 0.0f && (input & RIGHT)))) { @@ -154,11 +166,16 @@ struct Character : Controller { stand = getStand(); updateState(); Controller::update(); - updateVelocity(); - updatePosition(); - if (p != pos) { - updateLights(); - updateZone(); + + if (getEntity().flags.active) { + updateVelocity(); + updatePosition(); + if (p != pos) { + if (updateZone()) + updateLights(); + else + pos = p; + } } } diff --git a/src/controller.h b/src/controller.h index f0a8914..769d8e3 100644 --- a/src/controller.h +++ b/src/controller.h @@ -8,7 +8,6 @@ #include "collision.h" #define GRAVITY (6.0f * 30.0f) -#define NO_OVERLAP 0x7FFFFFFF #define SPRITE_FPS 10.0f #define MAX_LAYERS 4 @@ -17,9 +16,11 @@ struct Controller; struct IGame { virtual ~IGame() {} - virtual TR::Level* getLevel() { return NULL; } - virtual MeshBuilder* getMesh() { return NULL; } - virtual Controller* getCamera() { return NULL; } + virtual TR::Level* getLevel() { return NULL; } + virtual MeshBuilder* getMesh() { return NULL; } + virtual Controller* getCamera() { return NULL; } + virtual uint16 getRandomBox(uint16 zone, uint16 *zones) { return 0; } + virtual uint16 findPath(int ascend, int descend, int boxStart, int boxEnd, uint16 *zones, uint16 **boxes) { return 0; } virtual void setClipParams(float clipSign, float clipHeight) {} virtual void setWaterParams(float height) {} virtual void updateParams() {} @@ -178,38 +179,6 @@ struct Controller { return pos; } - int getOverlap(int fromX, int fromY, int fromZ, int toX, int toZ) const { - int dx, dz; - TR::Room::Sector &s = level->getSector(getEntity().room, fromX, fromZ, dx, dz); - - if (s.boxIndex == 0xFFFF) - return NO_OVERLAP; - - TR::Box &b = level->boxes[s.boxIndex]; - if (b.contains(toX, toZ)) - return 0; - - int floor = NO_OVERLAP; - int delta = NO_OVERLAP; - - TR::Overlap *o = &level->overlaps[b.overlap.index]; - do { - TR::Box &ob = level->boxes[o->boxIndex]; - if (ob.contains(toX, toZ)) { // get min delta - int d = abs(b.floor - ob.floor); - if (d < delta) { - floor = ob.floor; - delta = d; - } - } - } while (!(o++)->end); - - if (floor == NO_OVERLAP) - return NO_OVERLAP; - - return b.floor - floor; - } - Sound::Sample* playSound(int id, const vec3 &pos, int flags) const { if (level->version == TR::Level::VER_TR1_PSX && id == TR::SND_SECRET) return NULL; @@ -276,12 +245,50 @@ struct Controller { return true; } - virtual Box getBoundingBox() { + Box getBoundingBox() { return getBoundingBoxLocal() * getMatrix(); } - virtual Box getBoundingBoxLocal() { - return animation.getBoundingBox(vec3(0, 0, 0), 0); + Box getBoundingBoxLocal(bool oriented = false) { + return animation.getBoundingBox(vec3(0, 0, 0), oriented ? getEntity().rotation.value / 0x4000 : 0); + } + + void getSpheres(Sphere *spheres) { + TR::Model *m = getModel(); + Basis basis(getMatrix()); + + for (int i = 0; i < m->mCount; i++) { + TR::Mesh &aMesh = level->meshes[level->meshOffsets[m->mStart + i]]; + vec3 center = animation.getJoints(basis, i, true) * aMesh.center; + spheres[i] = Sphere(center, aMesh.radius); + } + + } + + bool collide(Controller *controller) { + TR::Model *a = getModel(); + TR::Model *b = getModel(); + if (!a || !b) + return false; + + if (!getBoundingBox().intersect(controller->getBoundingBox())) + return false; + + ASSERT(a->mCount <= 34); + ASSERT(b->mCount <= 34); + + Sphere aSpheres[34]; + Sphere bSpheres[34]; + + getSpheres(aSpheres); + controller->getSpheres(bSpheres); + + for (int i = 0; i < a->mCount; i++) + for (int j = 0; j < b->mCount; j++) + if (aSpheres[i].intersect(bSpheres[j])) + return true; + + return false; } vec3 trace(int fromRoom, const vec3 &from, const vec3 &to, int &room, bool isCamera) { // TODO: use Bresenham diff --git a/src/debug.h b/src/debug.h index 6e64a10..1a5d5f0 100644 --- a/src/debug.h +++ b/src/debug.h @@ -288,11 +288,11 @@ namespace Debug { for (int x = 0; x < r.xSectors; x++) { TR::Room::Sector &s = r.sectors[x * r.zSectors + z]; if (s.boxIndex != 0xFFFF) { - bool blockable = level.boxes[s.boxIndex].overlap.value & 0x8000; - bool block = level.boxes[s.boxIndex].overlap.value & 0x4000; + bool blockable = level.boxes[s.boxIndex].overlap.blockable; + bool block = level.boxes[s.boxIndex].overlap.block; int floor = level.boxes[s.boxIndex].floor; - if (blockable || block) { + if (blockable || block) { sprintf(buf, "blocked: %s", block ? "true" : "false"); Debug::Draw::text(vec3(r.info.x + x * 1024 + 512, floor, r.info.z + z * 1024 + 512), vec4(1, 1, 0, 1), buf); } @@ -304,10 +304,22 @@ namespace Debug { } void debugOverlaps(const TR::Level &level, int boxIndex) { - glColor4f(1.0f, 1.0f, 0.0f, 0.25f); + char str[64]; + + TR::Box &b = level.boxes[boxIndex]; + sprintf(str, "%d", boxIndex); + Draw::text(vec3((b.maxX + b.minX) * 0.5, b.floor, (b.maxZ + b.minZ) * 0.5), vec4(0, 1, 0, 1), str); + glColor4f(0.0f, 1.0f, 0.0f, 0.25f); + Core::setBlending(bmAlpha); + debugBox(b); + TR::Overlap *o = &level.overlaps[level.boxes[boxIndex].overlap.index]; do { TR::Box &b = level.boxes[o->boxIndex]; + sprintf(str, "%d", o->boxIndex); + Draw::text(vec3((b.maxX + b.minX) * 0.5, b.floor, (b.maxZ + b.minZ) * 0.5), vec4(0, 0, 1, 1), str); + glColor4f(0.0f, 0.0f, 1.0f, 0.25f); + Core::setBlending(bmAlpha); debugBox(b); } while (!(o++)->end); } @@ -369,7 +381,7 @@ namespace Debug { for (int i = 0; i < level.entitiesCount; i++) { TR::Entity &e = level.entities[i]; - sprintf(buf, "%d", (int)e.type); + sprintf(buf, "%d (%d)", (int)e.type, i); Debug::Draw::text(vec3(e.x, e.y, e.z), e.flags.active ? vec4(0, 0, 0.8, 1) : vec4(0.8, 0, 0, 1), buf); } @@ -381,6 +393,24 @@ namespace Debug { } } + void path(TR::Level &level, Enemy *enemy) { + Enemy::Path *path = enemy->path; + + if (!path || !enemy->target) return; + for (int i = 0; i < path->count; i++) { + TR::Box &b = level.boxes[path->boxes[i]]; + if (i == path->index) + glColor4f(0.5, 0.5, 0.0, 0.5); + else + glColor4f(0.0, 0.5, 0.0, 0.5); + debugBox(b); + } + + Core::setDepthTest(false); + Draw::point(enemy->waypoint, vec4(1.0)); + Core::setDepthTest(true); + } + void zones(const TR::Level &level, Lara *lara) { Core::setDepthTest(false); for (int i = 0; i < level.roomsCount; i++) @@ -446,12 +476,13 @@ namespace Debug { if (!level.meshOffsets[sm->mesh]) continue; const TR::Mesh &mesh = level.meshes[level.meshOffsets[sm->mesh]]; - + /* { char buf[255]; sprintf(buf, "flags %d", (int)mesh.flags.value); Debug::Draw::text(offset + (box.min + box.max) * 0.5f, vec4(0.5, 0.5, 1.0, 1), buf); } + */ } } @@ -462,76 +493,29 @@ namespace Debug { if (!controller) continue; mat4 matrix = controller->getMatrix(); - Box box = controller->animation.getBoundingBox(vec3(0.0f), 0); + Basis basis(matrix); + + TR::Model *m = controller->getModel(); + if (!m) continue; + + Box box = controller->getBoundingBoxLocal(); Debug::Draw::box(matrix, box.min, box.max, vec4(1.0)); + Sphere spheres[34]; + ASSERT(m->mCount <= 34); + controller->getSpheres(spheres); - for (int j = 0; j < level.modelsCount; j++) { - TR::Model &m = level.models[j]; - TR::Node *node = m.node < level.nodesDataSize ? (TR::Node*)&level.nodesData[m.node] : NULL; - - if (!node) continue; // ??? - if (e.type == m.type) { - ASSERT(m.animation < 0xFFFF); - - int fSize = sizeof(TR::AnimFrame) + m.mCount * sizeof(uint16) * 2; - - TR::Animation *anim = controller->animation; - TR::AnimFrame *frame = (TR::AnimFrame*)&level.frameData[(anim->frameOffset + (controller ? int((controller->animation.time * 30.0f / anim->frameRate)) * fSize : 0) >> 1)]; - - //mat4 m; - //m.identity(); - //m.translate(vec3(frame->x, frame->y, frame->z).lerp(vec3(frameB->x, frameB->y, frameB->z), k)); - - int sIndex = 0; - mat4 stack[20]; - mat4 joint; - - joint.identity(); - if (frame) joint.translate(frame->pos); - - for (int k = 0; k < m.mCount; k++) { - - if (k > 0 && node) { - TR::Node &t = node[k - 1]; - - if (t.flags & 0x01) joint = stack[--sIndex]; - if (t.flags & 0x02) stack[sIndex++] = joint; - - ASSERT(sIndex >= 0 && sIndex < 20); - - joint.translate(vec3(t.x, t.y, t.z)); - } - - vec3 a = frame ? frame->getAngle(k) : vec3(0.0f); - - mat4 rot; - rot.identity(); - rot.rotateY(a.y); - rot.rotateX(a.x); - rot.rotateZ(a.z); - - joint = joint * rot; - - int offset = level.meshOffsets[m.mStart + k]; - TR::Mesh *mesh = (TR::Mesh*)&level.meshes[offset]; - //if (!mesh->flags) continue; - Debug::Draw::sphere(matrix * joint * mesh->center, mesh->radius, vec4(0, 1, 1, 0.5f)); - - { //if (e.id != 0) { - char buf[255]; - sprintf(buf, "(%d) radius %d flags %d", (int)e.type, (int)mesh->radius, (int)mesh->flags.value); - Debug::Draw::text(matrix * joint * mesh->center, vec4(0.5, 1, 0.5, 1), buf); - } - - } - Debug::Draw::box(matrix, frame->box.min(), frame->box.max(), vec4(1.0)); - - break; + for (int joint = 0; joint < m->mCount; joint++) { + Sphere &sphere = spheres[joint]; + Debug::Draw::sphere(sphere.center, sphere.radius, vec4(0, 1, 1, 0.5f)); + /* + { //if (e.id != 0) { + char buf[255]; + sprintf(buf, "(%d) radius %d flags %d", (int)e.type, (int)mesh->radius, (int)mesh->flags.value); + Debug::Draw::text(sphere.center, vec4(0.5, 1, 0.5, 1), buf); } - + */ } - } } diff --git a/src/enemy.h b/src/enemy.h index 4521e80..5f995a4 100644 --- a/src/enemy.h +++ b/src/enemy.h @@ -4,9 +4,77 @@ #include "character.h" struct Enemy : Character { - bool bitten; - Enemy(IGame *game, int entity, int health) : Character(game, entity, health), bitten(false) {} + struct Path { + int16 index; + int16 count; + uint16 *boxes; + TR::Level *level; + + Path(TR::Level *level, uint16 *boxes, int count) : index(0), count(count), boxes(new uint16[count]), level(level) { + memcpy(this->boxes, boxes, count * sizeof(boxes[0])); + } + + ~Path() { + delete[] boxes; + } + + bool getNextPoint(TR::Level *level, vec3 &point) { + if (index >= count - 1) + return false; + + TR::Box &a = level->boxes[boxes[index++]]; + TR::Box &b = level->boxes[boxes[index]]; + + int minX = max(a.minX, b.minX); + int minZ = max(a.minZ, b.minZ); + int maxX = min(a.maxX, b.maxX); + int maxZ = min(a.maxZ, b.maxZ); + + point.x = float(minX + 512) + randf() * (maxX - minX - 1024); + point.y = float((a.floor + b.floor) / 2); + point.z = float(minZ + 512) + randf() * (maxZ - minZ - 1024); + + return true; + } + }; + + enum AI { + AI_FIXED, AI_RANDOM + } ai; + + enum Mood { + MOOD_SLEEP, MOOD_STALK, MOOD_ATTACK, MOOD_ESCAPE + } mood; + + bool wound; + int nextState; + + int targetBox; + vec3 waypoint; + + float thinkTime; + float aggression; + int radius; + int stepHeight; + int dropHeight; + + Character *target; + Path *path; + + int jointChest; + int jointHead; + + Enemy(IGame *game, int entity, int health, int radius, float aggression) : Character(game, entity, health), ai(AI_RANDOM), mood(MOOD_SLEEP), wound(false), nextState(0), targetBox(-1), thinkTime(0.0f), aggression(aggression), radius(radius), target(NULL), path(NULL) { + stepHeight = 256; + dropHeight = -256; + + jointChest = jointHead = -1; + } + + virtual ~Enemy() { + delete path; + } virtual bool activate(ActionCommand *cmd) { #ifdef LEVEL_EDITOR @@ -15,14 +83,15 @@ struct Enemy : Character { Controller::activate(cmd); - getEntity().flags.active = true; + getEntity().flags.active = true; activateNext(); for (int i = 0; i < level->entitiesCount; i++) if (level->entities[i].type == TR::Entity::LARA) { - target = i; + target = (Character*)level->entities[i].controller; break; } + ASSERT(target); return true; } @@ -31,22 +100,71 @@ struct Enemy : Character { velocity = getDir() * animation.getSpeed(); } + bool checkPoint(int x, int z) { + TR::Box &a = level->boxes[box]; + if (a.contains(x, z)) + return true; + + TR::Overlap *o = &level->overlaps[a.overlap.index]; + do { + TR::Box &b = level->boxes[o->boxIndex]; + if (!b.contains(x, z)) + continue; + if (getZones()[o->boxIndex] == zone) { + int d = a.floor - b.floor; + if (d <= stepHeight && d >= dropHeight) + return true; + } + } while (!(o++)->end); + + return false; + } + + void clipByBox(vec3 &pos) { + int px = int(pos.x); + int pz = int(pos.z); + int nx = px; + int nz = pz; + + TR::Box &a = level->boxes[box]; + + if (!checkPoint(px - radius, pz)) nx = a.minX + radius; + if (!checkPoint(px + radius, pz)) nx = a.maxX - radius; + if (!checkPoint(px, pz - radius)) nz = a.minZ + radius; + if (!checkPoint(px, pz + radius)) nz = a.maxZ - radius; + + if (px != nx) pos.x = float(nx); + if (pz != nz) pos.z = float(nz); + } + virtual void updatePosition() { if (!getEntity().flags.active) return; + vec3 p = pos; pos += velocity * Core::deltaTime * 30.0f; + + clipByBox(pos); + TR::Level::FloorInfo info; level->getFloorInfo(getRoomIndex(), (int)pos.x, (int)pos.y, (int)pos.z, info); - if (pos.y - info.floor > 1024) { - pos = p; - return; - } - switch (stand) { - case STAND_GROUND : pos.y = float(info.floor); break; - case STAND_AIR : pos.y = clamp(pos.y, float(info.ceiling), float(info.floor)); break; - default : ; - } + + int dx, dz; + TR::Room::Sector &s = level->getSector(info.roomNext != TR::NO_ROOM ? info.roomNext : getRoomIndex(), int(pos.x), int(pos.z), dx, dz); + if (s.boxIndex != 0xFFFF && zone == getZones()[s.boxIndex]) { + switch (stand) { + case STAND_GROUND : { + float fallSpeed = 2048.0f * Core::deltaTime; + decrease(info.floor - pos.y, pos.y, fallSpeed); + break; + } + case STAND_AIR : + pos.y = clamp(pos.y, float(info.ceiling), float(info.floor)); + break; + default : ; + } + } else + pos = p; updateEntity(); checkRoom(); @@ -88,11 +206,7 @@ struct Enemy : Character { } bool getTargetInfo(int height, vec3 *pos, float *angleX, float *angleY, float *dist) { - if (target == -1) return false; - Character *character = (Character*)level->entities[target].controller; - if (character->health <= 0) return false; - - vec3 p = character->pos; + vec3 p = waypoint; p.y -= height; if (pos) *pos = p; vec3 a = p - this->pos; @@ -109,11 +223,16 @@ struct Enemy : Character { } int turn(float delta, float speed) { - speed *= Core::deltaTime; - decrease(delta, angle.y, speed); - if (speed != 0.0f) { - velocity = velocity.rotateY(-speed); - return speed < 0 ? LEFT : RIGHT; + float w = speed * Core::deltaTime; + + updateTilt(delta, w, speed * 0.1f); + + if (delta != 0.0f) { + decrease(delta, angle.y, w); + if (speed != 0.0f) { + velocity = velocity.rotateY(-w); + return speed < 0 ? LEFT : RIGHT; + } } return 0; } @@ -128,21 +247,230 @@ struct Enemy : Character { return 0; } + + virtual void hit(int damage, Controller *enemy = NULL) { + Character::hit(damage, enemy); + wound = true; + }; + void bite(const vec3 &pos, int damage) { - if (bitten) return; - bitten = true; - ASSERT(target > -1); - Character *c = (Character*)level->entities[target].controller; - c->hit(damage, this); - Sprite::add(game, TR::Entity::BLOOD, c->getRoomIndex(), (int)pos.x, (int)pos.y, (int)pos.z, Sprite::FRAME_ANIMATED); + ASSERT(target); + target->hit(damage, this); + Sprite::add(game, TR::Entity::BLOOD, target->getRoomIndex(), (int)pos.x, (int)pos.y, (int)pos.z, Sprite::FRAME_ANIMATED); + } + + #define STALK_BOX (1024 * 3) + #define ESCAPE_BOX (1024 * 5) + #define ATTACK_BOX STALK_BOX + + Mood getMoodFixed() { + bool inZone = zone == target->zone; + + if (mood == MOOD_SLEEP || mood == MOOD_STALK) + return inZone ? MOOD_ATTACK : (wound ? MOOD_ESCAPE : mood); + + if (mood == MOOD_ATTACK) + return inZone ? mood : MOOD_SLEEP; + + return inZone ? MOOD_ATTACK : mood; + } + + Mood getMoodRandom() { + bool inZone = zone == target->zone; + bool brave = rand() < (mood != MOOD_ESCAPE ? 0x7800 : 0x0100) && inZone; + + if (mood == MOOD_SLEEP || mood == MOOD_STALK) { + if (wound && !brave) + return MOOD_ESCAPE; + if (inZone) { + int dx = abs(int(pos.x - target->pos.x)); + int dz = abs(int(pos.z - target->pos.z)); + return ((dx <= ATTACK_BOX && dz <= ATTACK_BOX) || (mood == MOOD_STALK && targetBox == -1)) ? MOOD_ATTACK : MOOD_STALK; + } + return mood; + } + + if (mood == MOOD_ATTACK) + return (wound && !brave) ? MOOD_ESCAPE : (!inZone ? MOOD_SLEEP : mood); + + return brave ? MOOD_STALK : mood; + } + + bool think(bool fixedLogic) { + thinkTime += Core::deltaTime; + if (thinkTime < 1.0f / 30.0f) + return false; + thinkTime -= 1.0f / 30.0f; + + if (!target) { + mood = MOOD_SLEEP; + return true; + } + + int targetBoxOld = targetBox; + + // update mood + bool inZone = zone == target->zone; + + if (mood != MOOD_ATTACK && targetBox > -1 && !checkBox(targetBox)) { + if (!inZone) + mood = MOOD_SLEEP; + targetBox = -1; + } + + mood = target->health <= 0 ? MOOD_SLEEP : (ai == AI_FIXED ? getMoodFixed() : getMoodRandom()); + + // set behavior and target + int box; + + switch (mood) { + case MOOD_SLEEP : + if (targetBox == -1 && checkBox(box = getRandomZoneBox()) && isStalkBox(box)) { + mood = MOOD_STALK; + gotoBox(box); + } + break; + case MOOD_STALK : + if ((targetBox == -1 || !isStalkBox(targetBox)) && checkBox(box = getRandomZoneBox())) { + if (isStalkBox(box)) + gotoBox(box); + else + if (targetBox == -1) { + if (!inZone) + mood = MOOD_SLEEP; + gotoBox(box); + } + } + break; + case MOOD_ATTACK : + if (randf() > aggression) + break; + targetBox = -1; + break; + case MOOD_ESCAPE : + if (targetBox == -1 && checkBox(box = getRandomZoneBox())) { + if (isEscapeBox(box)) + gotoBox(box); + else + if (inZone && isStalkBox(box)) { + mood = MOOD_STALK; + gotoBox(box); + } + } + break; + } + + if (targetBox == -1) + gotoBox(target->box); + + if (path && this->box != path->boxes[path->index - 1] && this->box != path->boxes[path->index]) + targetBoxOld = -1; + + if (targetBoxOld != targetBox) { + if (findPath(stepHeight, dropHeight)) + nextWaypoint(); + else + targetBox = -1; + } + + if (targetBox != -1 && path) { + vec3 d = pos - waypoint; + + if (fabsf(d.x) < 512 && fabsf(d.y) < 512 && fabsf(d.z) < 512) + nextWaypoint(); + } + + return true; + } + + void nextWaypoint() { + if (!path->getNextPoint(level, waypoint)) + waypoint = target->pos; + if (flying) { + if (target->stand != STAND_ONWATER) + waypoint.y -= 765.0f; + else + waypoint.y -= 64.0f; + } + } + + uint16 getRandomZoneBox() { + return game->getRandomBox(zone, getZones()); + } + + void gotoBox(int box) { + targetBox = box; + } + + bool checkBox(int box) { + if (zone != getZones()[box]) + return false; + + TR::Entity &e = getEntity(); + TR::Box &b = game->getLevel()->boxes[box]; + TR::Entity::Type type = e.type; + + if (type == TR::Entity::ENEMY_REX || type == TR::Entity::ENEMY_MUTANT_1 || type == TR::Entity::ENEMY_CENTAUR) { + if (b.overlap.blockable) + return false; + } else + if (b.overlap.block) + return false; + + return e.x < int(b.minX) || e.x > int(b.maxX) || e.z < int(b.minZ) || e.z > int(b.maxZ); + } + + bool isStalkBox(int box) { + TR::Entity &t = target->getEntity(); + TR::Box &b = game->getLevel()->boxes[box]; + + int x = (b.minX + b.maxX) / 2 - t.x; + if (abs(x) > STALK_BOX) return false; + + int z = (b.minZ + b.maxZ) / 2 - t.z; + if (abs(z) > STALK_BOX) return false; + + // TODO: check for some quadrant shit + + return true; + } + + bool isEscapeBox(int box) { + TR::Entity &e = getEntity(); + TR::Entity &t = target->getEntity(); + TR::Box &b = game->getLevel()->boxes[box]; + + int x = (b.minX + b.maxX) / 2 - t.x; + if (abs(x) < ESCAPE_BOX) return false; + + int z = (b.minZ + b.maxZ) / 2 - t.z; + if (abs(z) < ESCAPE_BOX) return false; + + return !((e.x > t.x) ^ (x > 0)) || !((e.z > t.z) ^ (z > 0)); + } + + bool findPath(int ascend, int descend) { + delete path; + path = NULL; + + uint16 *boxes; + uint16 count = game->findPath(ascend, descend, box, targetBox, getZones(), &boxes); + if (count) { + path = new Path(level, boxes, count); + return true; + } + + return false; } }; +#define WOLF_TURN_FAST (DEG2RAD * 150) +#define WOLF_TURN_SLOW (DEG2RAD * 60) + +#define WOLF_DIST_STALK STALK_BOX +#define WOLF_DIST_BITE 345 +#define WOLF_DIST_ATTACK (1024 + 512) -#define WOLF_TURN_FAST PI -#define WOLF_TURN_SLOW (PI / 3.0f) -#define WOLF_TILT_MAX (PI / 6.0f) -#define WOLF_TILT_SPEED WOLF_TILT_MAX struct Wolf : Enemy { @@ -153,70 +481,110 @@ struct Wolf : Enemy { }; enum { - STATE_STOP = 1, - STATE_WALK = 2, - STATE_RUN = 3, - STATE_STALKING = 5, - STATE_JUMP = 6, - STATE_HOWL = 7, - STATE_SLEEP = 8, - STATE_GROWL = 9, - STATE_TURN = 10, - STATE_DEATH = 11, - STATE_BITE = 12, + STATE_NONE , + STATE_STOP , + STATE_WALK , + STATE_RUN , + STATE_JUMP , // unused + STATE_STALK , + STATE_ATTACK , + STATE_HOWL , + STATE_SLEEP , + STATE_GROWL , + STATE_TURN , // unused + STATE_DEATH , + STATE_BITE , }; - enum { - JOINT_CHEST = 2, - JOINT_HEAD = 3 - }; - - Wolf(IGame *game, int entity) : Enemy(game, entity, 6) {} + Wolf(IGame *game, int entity) : Enemy(game, entity, 6, 341, 0.25f) { + dropHeight = -1024; + jointChest = 2; + jointHead = 3; + nextState = STATE_NONE; + } virtual int getStateGround() { TR::Entity &e = getEntity(); if (!e.flags.active) return (state == STATE_STOP || state == STATE_SLEEP) ? STATE_SLEEP : STATE_STOP; + if (!think(false)) + return state; + + float angle, dist; + getTargetInfo(0, NULL, NULL, &angle, NULL); + + dist = (target && target->health > 0) ? (pos - target->pos).length() : +INF; + + bool inZone = target ? target->zone == zone : false; + + if (nextState == state) + nextState = STATE_NONE; + switch (state) { - case STATE_SLEEP : return (target > -1 && level->entities[target].room == getRoomIndex()) ? STATE_STOP : state; - case STATE_STOP : return target > -1 ? STATE_HOWL : STATE_SLEEP; - case STATE_HOWL : return state; - case STATE_GROWL : return target > -1 ? (randf() > 0.5f ? STATE_STALKING : STATE_RUN) : STATE_STOP; - case STATE_RUN : - case STATE_STALKING : { - if (state == STATE_STALKING && health < 6) return STATE_RUN; - - float angleY, dist; - if (getTargetInfo(0, NULL, NULL, &angleY, &dist)) { - float w = state == STATE_RUN ? WOLF_TURN_FAST : WOLF_TURN_SLOW; - input = turn(angleY, w); // also set input mask (left, right) for tilt control - - if ((state == STATE_STALKING && dist < 512)) { - bitten = false; - return STATE_BITE; - } - if ((state == STATE_RUN && dist > 512 && dist < 1024)) { - bitten = false; - return STATE_JUMP; - } - } else { - target = -1; - return STATE_GROWL; + case STATE_SLEEP : + if (mood == MOOD_ESCAPE || inZone) { + nextState = STATE_GROWL; + return STATE_STOP; + } + if (randf() < 0.0001f) { + nextState = STATE_WALK; + return STATE_STOP; } break; - } + case STATE_STOP : return nextState != STATE_NONE ? nextState : STATE_WALK; + case STATE_WALK : + if (mood != MOOD_SLEEP) { + nextState = STATE_NONE; + return STATE_STALK; + } + if (randf() < 0.0001f) { + nextState = STATE_SLEEP; + return STATE_STOP; + } + break; + case STATE_GROWL : + if (nextState != STATE_NONE) return nextState; + if (mood == MOOD_ESCAPE) return STATE_RUN; + if (dist < WOLF_DIST_BITE) return STATE_BITE; + if (mood == MOOD_STALK) return STATE_STALK; + if (mood == MOOD_SLEEP) return STATE_STOP; + return STATE_RUN; + case STATE_STALK : + if (mood == MOOD_ESCAPE) return STATE_RUN; + if (dist < WOLF_DIST_BITE) return STATE_BITE; + if (dist > WOLF_DIST_STALK) return STATE_RUN; + if (mood == MOOD_ATTACK) return STATE_RUN; + if (randf() < 0.012f) { + nextState = STATE_HOWL; + return STATE_GROWL; + } + if (mood == MOOD_SLEEP) return STATE_GROWL; + break; + case STATE_RUN : + if (dist < WOLF_DIST_ATTACK) { + if (dist < WOLF_DIST_ATTACK * 0.5f && fabsf(angle) < PI * 0.5f) { + nextState = STATE_NONE; + return STATE_ATTACK; + } + nextState = STATE_STALK; + return STATE_GROWL; + } + if (mood == MOOD_STALK && dist < WOLF_DIST_STALK) { + nextState = STATE_STALK; + return STATE_GROWL; + } + if (mood == MOOD_SLEEP) return STATE_GROWL; + break; + case STATE_ATTACK : + case STATE_BITE : + if (nextState == STATE_NONE && target->health > 0 && collide(target)) { + bite(animation.getJoints(getMatrix(), jointHead, true).pos, state == STATE_ATTACK ? 50 : 100); + nextState = state == STATE_ATTACK ? STATE_RUN : STATE_GROWL; + } + return state == STATE_ATTACK ? STATE_RUN : state; } - if ((state == STATE_JUMP || state == STATE_BITE) && !bitten) { - float dist; - if (getTargetInfo(0, NULL, NULL, NULL, &dist) && dist < 256.0f) - bite(animation.getJoints(getMatrix(), JOINT_HEAD, true).pos, state == STATE_BITE ? 100 : 50); - } - - if (state == STATE_JUMP) - return STATE_RUN; - return state; } @@ -230,18 +598,30 @@ struct Wolf : Enemy { } virtual void updatePosition() { - updateTilt(state == STATE_RUN, WOLF_TILT_SPEED, WOLF_TILT_MAX); + float angleY = 0.0f; + if (state == STATE_RUN || state == STATE_WALK || state == STATE_STALK) + getTargetInfo(0, NULL, NULL, &angleY, NULL); + + turn(angleY, state == STATE_RUN ? WOLF_TURN_FAST : WOLF_TURN_SLOW); if (state == STATE_DEATH) { animation.overrideMask = 0; return; } + Enemy::updatePosition(); - setOverrides(state == STATE_STALKING || state == STATE_RUN, JOINT_CHEST, JOINT_HEAD); - lookAt(target, JOINT_CHEST, JOINT_HEAD); + setOverrides(state == STATE_RUN || state == STATE_WALK || state == STATE_STALK, jointChest, jointHead); + lookAt(target ? target->entity : -1, jointChest, jointHead); } }; +#define BEAR_DIST_EAT 768 +#define BEAR_DIST_HOWL 2048 +#define BEAR_DIST_BITE 1024 +#define BEAR_DIST_ATTACK 600 + +#define BEAR_TURN_FAST (DEG2RAD * 150) +#define BEAR_TURN_SLOW (DEG2RAD * 60) struct Bear : Enemy { @@ -251,130 +631,185 @@ struct Bear : Enemy { }; enum { - STATE_WALK = 0, - STATE_STOP = 1, - STATE_HIND = 2, - STATE_RUN = 3, - STATE_HOWL = 4, - STATE_GROWL = 5, - STATE_BITE = 6, - STATE_ATTACK = 7, - STATE_EAT = 8, - STATE_DEATH = 9, + STATE_NONE = -1, + STATE_WALK , + STATE_STOP , + STATE_HIND , + STATE_RUN , + STATE_HOWL , + STATE_GROWL , + STATE_BITE , + STATE_ATTACK , + STATE_EAT , + STATE_DEATH , }; - enum { - JOINT_CHEST = 2, - JOINT_HEAD = 3 - }; - - Bear(IGame *game, int entity) : Enemy(game, entity, 20) {} + Bear(IGame *game, int entity) : Enemy(game, entity, 20, 341, 0.5f) { + jointChest = 13; + jointHead = 14; + nextState = STATE_NONE; + } virtual int getStateGround() { + if (!getEntity().flags.active) + return state; + + if (!think(false)) + return state; + + if (nextState == state) + nextState = STATE_NONE; + + float dist = target ? (pos - target->pos).length() : +INF; + + bool targetDead = target->health <= 0; + switch (state) { - case STATE_STOP : return STATE_RUN; - case STATE_GROWL : return state; - case STATE_WALK : - case STATE_RUN : - case STATE_HIND : { - if (state == STATE_HIND && health < 6) return STATE_RUN; - - float angleY, dist; - if (getTargetInfo(0, NULL, NULL, &angleY, &dist)) { - float w = state == STATE_RUN ? WOLF_TURN_FAST : WOLF_TURN_SLOW; - input = turn(angleY, w); // also set input mask (left, right) for tilt control - - if ((state == STATE_HIND && dist < 512)) { - bitten = false; - return STATE_ATTACK; - } - if ((state == STATE_RUN && dist > 512 && dist < 1024)) { - bitten = false; - return STATE_BITE; + case STATE_WALK : + if (targetDead && collide(target)) + return STATE_STOP; // eat lara! >:E + else + if (mood != MOOD_SLEEP) { + if (mood == MOOD_ESCAPE) + nextState = STATE_NONE; + return STATE_STOP; + } else if (randf() < 0.003f) { + nextState = STATE_GROWL; + return STATE_STOP; } + break; + case STATE_STOP : + if (targetDead) + return dist <= BEAR_DIST_EAT ? STATE_EAT : STATE_WALK; + else + return nextState != STATE_NONE ? nextState : (mood == MOOD_SLEEP ? STATE_WALK : STATE_RUN); + case STATE_HIND : + if (collide(target)) { + return STATE_HOWL; + } + if (mood == MOOD_ESCAPE) { + nextState = STATE_NONE; + return STATE_HOWL; + } + if (mood == MOOD_SLEEP || randf() < 0.003f) { + nextState = STATE_GROWL; + return STATE_HOWL; + } + if (dist > BEAR_DIST_HOWL || randf() < 0.05f) { + nextState = STATE_STOP; + return STATE_HOWL; + } + break; + case STATE_RUN : + if (collide(target)) + target->hit(3, this); + if (targetDead || mood == MOOD_SLEEP) + return STATE_STOP; + if (dist < BEAR_DIST_HOWL && randf() < 0.025f) { + nextState = STATE_HOWL; + return STATE_STOP; + } else if (dist < BEAR_DIST_BITE) { + nextState = STATE_NONE; + return STATE_BITE; + } + break; + case STATE_HOWL : + if (nextState != STATE_NONE) return nextState; + if (mood == MOOD_SLEEP || mood == MOOD_ESCAPE) return STATE_STOP; + if (dist < BEAR_DIST_ATTACK) return STATE_ATTACK; + return STATE_HIND; + case STATE_BITE : + case STATE_ATTACK : + if (nextState == STATE_NONE && collide(target)) { + bite(animation.getJoints(getMatrix(), jointHead, true).pos, state == STATE_BITE ? 200 : 400); + nextState = state == STATE_BITE ? STATE_STOP : STATE_HOWL; } break; - } - } - - if ((state == STATE_ATTACK || state == STATE_BITE) && !bitten) { - float dist; - if (getTargetInfo(0, NULL, NULL, NULL, &dist) && dist < 256.0f) - bite(animation.getJoints(getMatrix(), JOINT_HEAD, true).pos, state == STATE_BITE ? 100 : 50); } return state; } virtual int getStateDeath() { - return state == STATE_DEATH ? state : animation.setAnim(ANIM_DEATH); + switch (state) { + case STATE_HIND : return STATE_HOWL; + case STATE_RUN : + case STATE_WALK : return STATE_STOP; + case STATE_HOWL : + case STATE_STOP : return STATE_DEATH; + } + return state;// == STATE_DEATH ? state : animation.setAnim(ANIM_DEATH); } virtual void updatePosition() { - updateTilt(state == STATE_RUN, WOLF_TILT_SPEED, WOLF_TILT_MAX); - Enemy::updatePosition(); - /* + float angleY = 0.0f; + if (state == STATE_RUN || state == STATE_WALK || state == STATE_HIND) + getTargetInfo(0, NULL, NULL, &angleY, NULL); + + turn(angleY, state == STATE_RUN ? BEAR_TURN_FAST : BEAR_TURN_SLOW); + if (state == STATE_DEATH) { animation.overrideMask = 0; return; } + Enemy::updatePosition(); - setOverrides(state == STATE_STALKING || state == STATE_RUN, JOINT_CHEST, JOINT_HEAD); - lookAt(target, JOINT_CHEST, JOINT_HEAD); - */ + setOverrides(state == STATE_RUN || state == STATE_WALK || state == STATE_HIND, jointChest, jointHead); + lookAt(target ? target->entity : -1, jointChest, jointHead); } }; -#define BAT_TURN_SPEED PI +#define BAT_TURN_SPEED (DEG2RAD * 300) #define BAT_LIFT_SPEED 512.0f struct Bat : Enemy { enum { - ANIM_DEATH = 4, + ANIM_DEATH = 4, }; enum { - STATE_AWAKE = 1, - STATE_FLY = 2, - STATE_ATTACK = 3, - STATE_CIRCLING = 4, - STATE_DEATH = 5, + STATE_NONE, + STATE_AWAKE, + STATE_FLY, + STATE_ATTACK, + STATE_CIRCLING, + STATE_DEATH, }; - Bat(IGame *game, int entity) : Enemy(game, entity, 1) { stand = STAND_AIR; } + Bat(IGame *game, int entity) : Enemy(game, entity, 1, 102, 0.03f) { + stand = STAND_AIR; + stepHeight = 20 * 1024; + dropHeight = -20 * 1024; + jointHead = 4; + } virtual int getStateAir() { if (!getEntity().flags.active) { animation.time = 0.0f; - animation.dir = 0.0f; + animation.dir = 0.0f; return STATE_AWAKE; } + if (!think(false)) + return state; + switch (state) { case STATE_AWAKE : return STATE_FLY; case STATE_ATTACK : - case STATE_FLY : { - vec3 p; - float angleY, dist; - if (getTargetInfo(765, &p, NULL, &angleY, &dist)) { - turn(angleY, BAT_TURN_SPEED); - lift(p.y - pos.y, BAT_LIFT_SPEED); - - if (dist < 128) { - if (state == STATE_ATTACK && !(animation.frameIndex % 15)) - bite(pos, 2); // TODO: bite position - else - bitten = false; - return STATE_ATTACK; - } else - return STATE_FLY; - } else { - turn(PI, BAT_TURN_SPEED); // circling + if (!collide(target)) { + mood = MOOD_SLEEP; return STATE_FLY; + } else + bite(animation.getJoints(getMatrix(), jointHead, true).pos, 2); + break; + case STATE_FLY : + if (collide(target)) { + mood = MOOD_ATTACK; + return STATE_ATTACK; } - } + break; } return state; @@ -391,6 +826,15 @@ struct Bat : Enemy { velocity = vec3(0.0f, velocity.y + GRAVITY * Core::deltaTime, 0.0f); } + virtual void updatePosition() { + float angleY = 0.0f; + if (state == STATE_FLY || state == STATE_ATTACK) + getTargetInfo(0, NULL, NULL, &angleY, NULL); + turn(angleY, BAT_TURN_SPEED); + if (flying) + lift(waypoint.y - pos.y, BAT_LIFT_SPEED); + Enemy::updatePosition(); + } }; #endif diff --git a/src/lara.h b/src/lara.h index 788d28a..aad5d25 100644 --- a/src/lara.h +++ b/src/lara.h @@ -216,6 +216,10 @@ struct Lara : Character { int roomPrev; // water out from room vec2 rotFactor; + float hitTime; + int hitDir; + vec3 collisionOffset; + struct Braid { Lara *lara; vec3 offset; @@ -386,6 +390,9 @@ struct Lara : Character { animation.setAnim(ANIM_STAND); } + hitDir = -1; + hitTime = 0.0f; + getEntity().flags.active = 1; initMeshOverrides(); @@ -888,8 +895,9 @@ struct Lara : Character { } void updateOverrides() { + int overrideMask = 0; // head & chest - animation.overrideMask |= BODY_CHEST | BODY_HEAD; + overrideMask |= BODY_CHEST | BODY_HEAD; animation.overrides[ 7] = animation.getJointRot( 7); animation.overrides[14] = animation.getJointRot(14); @@ -902,6 +910,7 @@ struct Lara : Character { } */ + // arms if (!emptyHands()) { // right arm Arm *arm = &arms[0]; @@ -914,10 +923,38 @@ struct Lara : Character { animation.overrides[12] = arm->animation.getJointRot(12); animation.overrides[13] = arm->animation.getJointRot(13); - animation.overrideMask |= (BODY_ARM_R | BODY_ARM_L); + overrideMask |= (BODY_ARM_R | BODY_ARM_L); } else - animation.overrideMask &= ~(BODY_ARM_R | BODY_ARM_L); + overrideMask &= ~(BODY_ARM_R | BODY_ARM_L); + // update hit anim + if (hitDir >= 0) { + Animation hitAnim = Animation(level, getModel()); + switch (hitDir) { + case 0 : hitAnim.setAnim(ANIM_HIT_FRONT, 0, false); break; + case 1 : hitAnim.setAnim(ANIM_HIT_LEFT, 0, false); break; + case 2 : hitAnim.setAnim(ANIM_HIT_BACK , 0, false); break; + case 3 : hitAnim.setAnim(ANIM_HIT_RIGHT, 0, false); break; + } + hitTime = min(hitTime, hitAnim.timeMax - EPS); + hitAnim.time = hitTime; + hitAnim.updateInfo(); + + overrideMask &= ~(BODY_CHEST | BODY_HEAD); + int hitMask = (BODY_UPPER | BODY_LOWER | BODY_HEAD) & ~overrideMask; + int index = 0; + while (hitMask) { + if (hitMask & 1) + animation.overrides[index] = hitAnim.getJointRot(index); + index++; + hitMask >>= 1; + } + + hitTime += Core::deltaTime; + overrideMask = BODY_UPPER | BODY_LOWER | BODY_HEAD; + } + + animation.overrideMask = overrideMask; lookAt(viewTarget); @@ -1000,16 +1037,16 @@ struct Lara : Character { int getTarget() { vec3 dir = getDir().normal(); - float dist = TARGET_MAX_DIST;// * TARGET_MAX_DIST; + float dist = TARGET_MAX_DIST; int index = -1; for (int i = 0; i < level->entitiesCount; i++) { TR::Entity &e = level->entities[i]; if (!e.flags.active || !e.isEnemy()) continue; - Character *controller = (Character*)e.controller; - if (controller->health <= 0) continue; + Character *enemy = (Character*)e.controller; + if (enemy->health <= 0) continue; - vec3 p = controller->pos; + vec3 p = enemy->getBoundingBox().center(); vec3 v = p - pos; if (dir.dot(v.normal()) <= 0.5f) continue; // target is out of sight -60..+60 degrees @@ -1078,8 +1115,6 @@ struct Lara : Character { virtual void hit(int damage, Controller *enemy = NULL) { health -= damage; - if (enemy && health > 0) - playSound(TR::SND_HIT, pos, Sound::PAN | Sound::REPLAY); }; bool waterOut() { @@ -1729,6 +1764,12 @@ struct Lara : Character { } } + virtual void update() { + collisionOffset = vec3(0.0f); + checkCollisions(); + Character::update(); + } + virtual void updateAnimation(bool commands) { Controller::updateAnimation(commands); updateWeapon(); @@ -1858,7 +1899,7 @@ struct Lara : Character { vTilt *= rotFactor.y; updateTilt(state == STATE_RUN || stand == STAND_UNDERWATER, vTilt.x, vTilt.y); - if (velocity.length() >= 1.0f) + if ((velocity + collisionOffset).length2() >= 1.0f) move(); if (getEntity().type != TR::Entity::LARA) { @@ -1879,32 +1920,47 @@ struct Lara : Character { return getEntity().type == TR::Entity::LARA ? pos : chestOffset; } - void move() { - //TR::Entity &e = getEntity(); - //TR::Level::FloorInfo info; - - //float f, c; - //bool canPassGap = true; - /* - if (velocity != 0.0f) { - vec3 dir = velocity.normal() * 128.0f; - vec3 p = pos + dir; - level->getFloorInfo(e.room, (int)p.x, (int)p.y, (int)p.z, info); - if (info.floor < p.y - (256 + 128) || info.ceiling > p.y - 768) { // wall - vec3 axis = dir.axisXZ(); - vec3 normal = (p - vec3(int(p.x / 1024.0f) * 1024.0f + 512.0f, p.y, int(p.z / 1024.0f) * 1024.0f + 512.0f)).axisXZ(); - LOG("%f %f = %f %f = %f\n", axis.x, axis.z, normal.x, normal.z, abs(axis.dot(normal))); - if (abs(axis.dot(normal)) > EPS) { - canPassGap = false; - } else { - updateEntity(); - checkRoom(); - return; - } - } + void checkCollisions() { + if (state == STATE_DEATH || stand != STAND_GROUND) { + hitDir = -1; + return; } - */ - vec3 vel = velocity * Core::deltaTime * 30.0f; + + // check enemies + for (int i = 0; i < level->entitiesCount; i++) { + TR::Entity &e = level->entities[i]; + if (!e.flags.active || !e.isEnemy()) continue; + Character *enemy = (Character*)e.controller; + if (enemy->health <= 0) continue; + + vec3 dir = pos - enemy->pos; + vec3 p = dir.rotateY(-enemy->angle.y); + + Box enemyBox = enemy->getBoundingBoxLocal(); + if (!enemyBox.contains(p)) + continue; + + // get shift + p = enemyBox.closestPoint2D(p); + p = (p.rotateY(enemy->angle.y) + enemy->pos) - pos; + collisionOffset += vec3(p.x, 0.0f, p.z); + + // get hit dir + if (hitDir == -1) { + if (health > 0) + playSound(TR::SND_HIT, pos, Sound::PAN); + hitTime = 0.0f; + } + + hitDir = angleQuadrant(dir.rotateY(angle.y + PI * 0.5f).angleY()); + return; + } + + hitDir = -1; + } + + void move() { + vec3 vel = velocity * Core::deltaTime * 30.0f + collisionOffset; vec3 opos(pos), offset(0.0f); float radius = stand == STAND_UNDERWATER ? LARA_RADIUS_WATER : LARA_RADIUS; diff --git a/src/level.h b/src/level.h index 983ad93..9db4aea 100644 --- a/src/level.h +++ b/src/level.h @@ -34,6 +34,7 @@ struct Level : IGame { ShaderCache *shaderCache; AmbientCache *ambientCache; WaterCache *waterCache; + ZoneCache *zoneCache; Sound::Sample *sndAmbient; Sound::Sample *sndUnderwater; @@ -51,6 +52,15 @@ struct Level : IGame { return camera; } + virtual uint16 getRandomBox(uint16 zone, uint16 *zones) { + ZoneCache::Item *item = zoneCache->getBoxes(zone, zones); + return item->boxes[int(randf() * item->count)]; + } + + virtual uint16 findPath(int ascend, int descend, int boxStart, int boxEnd, uint16 *zones, uint16 **boxes) { + return zoneCache->findPath(ascend, descend, boxStart, boxEnd, zones, boxes); + } + virtual void setClipParams(float clipSign, float clipHeight) { params->clipSign = clipSign; params->clipHeight = clipHeight; @@ -144,7 +154,7 @@ struct Level : IGame { case TR::Entity::ENEMY_CENTAUR : case TR::Entity::ENEMY_MUMMY : case TR::Entity::ENEMY_LARSON : - entity.controller = new Enemy(this, i, 100); + entity.controller = new Enemy(this, i, 100, 10, 0.0f); break; case TR::Entity::DOOR_1 : case TR::Entity::DOOR_2 : @@ -223,6 +233,7 @@ struct Level : IGame { ambientCache = Core::settings.ambient ? new AmbientCache(this) : NULL; waterCache = Core::settings.water ? new WaterCache(this) : NULL; + zoneCache = new ZoneCache(this); shadow = Core::settings.shadows ? new Texture(SHADOW_TEX_SIZE, SHADOW_TEX_SIZE, Texture::SHADOW, false) : NULL; initReflections(); @@ -253,6 +264,7 @@ struct Level : IGame { delete shadow; delete ambientCache; delete waterCache; + delete zoneCache; delete atlas; delete cube; @@ -809,7 +821,7 @@ struct Level : IGame { glPopMatrix(); */ - + /* Core::setDepthTest(false); glBegin(GL_LINES); glColor3f(1, 1, 1); @@ -817,11 +829,11 @@ struct Level : IGame { glVertex3fv((GLfloat*)&lara->mainLightPos); glEnd(); Core::setDepthTest(true); - + */ // Debug::Draw::sphere(lara->mainLightPos, lara->mainLightColor.w, vec4(1, 1, 0, 1)); - Box bbox = lara->getBoundingBox(); - Debug::Draw::box(bbox.min, bbox.max, vec4(1, 0, 1, 1)); + // Box bbox = lara->getBoundingBox(); + // Debug::Draw::box(bbox.min, bbox.max, vec4(1, 0, 1, 1)); Core::setBlending(bmAlpha); // Debug::Level::rooms(level, lara->pos, lara->getEntity().room); @@ -832,8 +844,10 @@ struct Level : IGame { // Core::setDepthTest(true); // Debug::Level::meshes(level); // Debug::Level::entities(level); - Debug::Level::zones(level, lara); - Debug::Level::blocks(level); + // Debug::Level::zones(level, lara); + // Debug::Level::blocks(level); + // Debug::Level::path(level, (Enemy*)level.entities[86].controller); + // Debug::Level::debugOverlaps(level, lara->box); Core::setBlending(bmNone); diff --git a/src/utils.h b/src/utils.h index 9592c16..bbb986d 100644 --- a/src/utils.h +++ b/src/utils.h @@ -102,7 +102,7 @@ int angleQuadrant(float angle) { } float decrease(float delta, float &value, float &speed) { - if (speed > 0.0f && fabsf(delta) > 0.01f) { + if (speed > 0.0f && fabsf(delta) > 0.001f) { if (delta > 0) speed = min(delta, speed); if (delta < 0) speed = max(delta, -speed); value += speed; @@ -228,9 +228,11 @@ struct vec3 { return vec3(x*c - z*s, y, x*s + z*c); } - float angle(const vec3 &v) { + float angle(const vec3 &v) const { return dot(v) / (length() * v.length()); } + + float angleY() const { return atan2f(z, x); } }; struct vec4 { @@ -806,24 +808,61 @@ struct Box { } } + bool contains(const vec3 &v) const { + return v.x >= min.x && v.x <= max.x && v.y >= min.y && v.y <= max.x && v.z >= min.z && v.z <= max.z; + } + + vec3 closestPoint2D(const vec3 &v) const { + float ax = v.x - min.x; + float bx = max.x - v.x; + float az = v.z - min.z; + float bz = max.z - v.z; + + vec3 p = v; + if (ax <= bx && ax <= az && ax <= bz) + p.x -= ax; + else if (bx <= ax && bx <= az && bx <= bz) + p.x += bx; + else if (az <= ax && az <= bx && az <= bz) + p.z -= az; + else + p.z += bz; + + return p; + } + bool intersect(const Box &box) const { return !((max.x < box.min.x || min.x > box.max.x) || (max.y < box.min.y || min.y > box.max.y) || (max.z < box.min.z || min.z > box.max.z)); } bool intersect(const vec3 &rayPos, const vec3 &rayDir, float &t) const { - float t1 = INF, t0 = -t1; + float t1 = INF, t0 = -t1; - for (int i = 0; i < 3; i++) - if (rayDir[i] != 0) { - float lo = (min[i] - rayPos[i]) / rayDir[i]; - float hi = (max[i] - rayPos[i]) / rayDir[i]; - t0 = ::max(t0, ::min(lo, hi)); - t1 = ::min(t1, ::max(lo, hi)); - } else - if (rayPos[i] < min[i] || rayPos[i] > max[i]) - return false; - t = t0; - return (t0 <= t1) && (t1 > 0); + for (int i = 0; i < 3; i++) + if (rayDir[i] != 0) { + float lo = (min[i] - rayPos[i]) / rayDir[i]; + float hi = (max[i] - rayPos[i]) / rayDir[i]; + t0 = ::max(t0, ::min(lo, hi)); + t1 = ::min(t1, ::max(lo, hi)); + } else + if (rayPos[i] < min[i] || rayPos[i] > max[i]) + return false; + t = t0; + return (t0 <= t1) && (t1 > 0); + } +}; + +struct Sphere { + vec3 center; + float radius; + + Sphere() {} + Sphere(const vec3 ¢er, float radius) : center(center), radius(radius) {} + + bool intersect(const Sphere &s) { + float d = (center - s.center).length2(); + float r = (radius + s.radius); + return d < r * r; } };