1
0
mirror of https://github.com/XProger/OpenLara.git synced 2025-04-29 07:09:49 +02:00
openlara/src/lara.h

3307 lines
122 KiB
C

#ifndef H_LARA
#define H_LARA
/*****************************************/
/* Desine sperare qui hic intras */
/*****************************************/
#include "character.h"
#include "trigger.h"
#include "sprite.h"
#include "enemy.h"
// TODO: slide to slide in WALL
// TODO: static sounds in LEVEL3A
// TODO: fix enemy head rotation glitches
#define TURN_FAST PI
#define TURN_FAST_BACK PI * 3.0f / 4.0f
#define TURN_NORMAL PI / 2.0f
#define TURN_SLOW PI / 3.0f
#define TURN_WATER_FAST (DEG2RAD * 150.0f)
#define TURN_WATER_SLOW (DEG2RAD * 60.0f)
#define TURN_WALL_Y (DEG2RAD * 150.0f)
#define TURN_WALL_X (DEG2RAD * 60.0f)
#define TURN_WALL_X_CLAMP (DEG2RAD * 35.0f)
#define LARA_TILT_SPEED (DEG2RAD * 37.5f)
#define LARA_TILT_MAX (DEG2RAD * 10.0f)
#define LARA_MAX_HEALTH 1000.0f
#define LARA_MAX_OXYGEN 60.0f
#define LARA_HANG_OFFSET 724
#define LARA_HEIGHT 762
#define LARA_HEIGHT_WATER 400
#define LARA_RADIUS 100.0f
#define LARA_RADIUS_WATER 300.0f
#define LARA_WATER_ACCEL 2.0f
#define LARA_SURF_SPEED 15.0f
#define LARA_SWIM_SPEED 50.0f
#define LARA_SWIM_FRICTION 1.0f
#define LARA_MIN_SPECULAR 0.03f
#define LARA_WET_SPECULAR 0.5f
#define LARA_WET_TIMER (LARA_WET_SPECULAR / 16.0f) // 4 sec
#define LARA_DAMAGE_TIME (40.0f / 30.0f)
#define PICKUP_FRAME_GROUND 40
#define PICKUP_FRAME_UNDERWATER 18
#define PUZZLE_FRAME 80
#define KEY_FRAME 110
#define MAX_TRIGGER_ACTIONS 64
#define DESCENT_SPEED 2048.0f
#define TARGET_MAX_DIST (8.0f * 1024.0f)
struct Lara : Character {
// http://www.tombraiderforums.com/showthread.php?t=148859
enum {
ANIM_STAND_LEFT = 2,
ANIM_STAND_RIGHT = 3,
ANIM_RUN_START = 6,
ANIM_STAND = 11,
ANIM_LANDING = 24,
ANIM_CLIMB_JUMP = 26,
ANIM_FALL_HANG = 28,
ANIM_SMASH_JUMP = 32,
ANIM_FALL_FORTH = 34,
ANIM_CLIMB_3 = 42,
ANIM_CLIMB_2 = 50,
ANIM_SMASH_RUN_LEFT = 53,
ANIM_SMASH_RUN_RIGHT = 54,
ANIM_RUN_ASCEND_LEFT = 55,
ANIM_RUN_ASCEND_RIGHT = 56,
ANIM_WALK_ASCEND_LEFT = 57,
ANIM_WALK_ASCEND_RIGHT = 58,
ANIM_WALK_DESCEND_RIGHT = 59,
ANIM_WALK_DESCEND_LEFT = 60,
ANIM_BACK_DESCEND_LEFT = 61,
ANIM_BACK_DESCEND_RIGHT = 62,
ANIM_SLIDE_FORTH = 70,
ANIM_FALL_BACK = 93,
ANIM_HANG = 96,
ANIM_STAND_NORMAL = 103,
ANIM_SLIDE_BACK = 105,
ANIM_UNDERWATER = 108,
ANIM_WATER_FALL = 112,
ANIM_TO_ONWATER = 114,
ANIM_TO_UNDERWATER = 119,
ANIM_HIT_FRONT = 125,
ANIM_HIT_BACK = 126,
ANIM_HIT_LEFT = 127,
ANIM_HIT_RIGHT = 128,
ANIM_DEATH_BOULDER = 139,
ANIM_STAND_ROLL_BEGIN = 146,
ANIM_STAND_ROLL_END = 147,
ANIM_DEATH_SPIKES = 149,
ANIM_HANG_SWING = 150,
ANIM_SWITCH_BIG_DOWN = 195,
ANIM_SWITCH_BIG_UP = 196,
ANIM_PUSH_BUTTON = 197,
};
// http://www.tombraiderforums.com/showthread.php?t=211681
enum {
STATE_WALK,
STATE_RUN,
STATE_STOP,
STATE_FORWARD_JUMP,
STATE_POSE,
STATE_FAST_BACK,
STATE_TURN_RIGHT,
STATE_TURN_LEFT,
STATE_DEATH,
STATE_FALL,
STATE_HANG,
STATE_REACH,
STATE_SPLAT,
STATE_TREAD,
STATE_FAST_TURN_14,
STATE_COMPRESS,
STATE_BACK,
STATE_SWIM,
STATE_GLIDE,
STATE_HANG_UP,
STATE_FAST_TURN,
STATE_STEP_RIGHT,
STATE_STEP_LEFT,
STATE_ROLL_1,
STATE_SLIDE,
STATE_BACK_JUMP,
STATE_RIGHT_JUMP,
STATE_LEFT_JUMP,
STATE_UP_JUMP,
STATE_FALL_BACK,
STATE_HANG_LEFT,
STATE_HANG_RIGHT,
STATE_SLIDE_BACK,
STATE_SURF_TREAD,
STATE_SURF_SWIM,
STATE_DIVE,
STATE_PUSH_BLOCK,
STATE_PULL_BLOCK,
STATE_PUSH_PULL_READY,
STATE_PICK_UP,
STATE_SWITCH_DOWN,
STATE_SWITCH_UP,
STATE_USE_KEY,
STATE_USE_PUZZLE,
STATE_UNDERWATER_DEATH,
STATE_ROLL_2,
STATE_SPECIAL,
STATE_SURF_BACK,
STATE_SURF_LEFT,
STATE_SURF_RIGHT,
STATE_MIDAS_USE,
STATE_MIDAS_DEATH,
STATE_SWAN_DIVE,
STATE_FAST_DIVE,
STATE_HANDSTAND,
STATE_WATER_OUT,
STATE_MAX };
#define LARA_RGUN_JOINT 10
#define LARA_LGUN_JOINT 13
#define LARA_RGUN_OFFSET vec3(-10, -50, 0)
#define LARA_LGUN_OFFSET vec3( 10, -50, 0)
enum {
BODY_HIP = 0x0001,
BODY_LEG_L1 = 0x0002,
BODY_LEG_L2 = 0x0004,
BODY_LEG_L3 = 0x0008,
BODY_LEG_R1 = 0x0010,
BODY_LEG_R2 = 0x0020,
BODY_LEG_R3 = 0x0040,
BODY_CHEST = 0x0080,
BODY_ARM_R1 = 0x0100,
BODY_ARM_R2 = 0x0200,
BODY_ARM_R3 = 0x0400,
BODY_ARM_L1 = 0x0800,
BODY_ARM_L2 = 0x1000,
BODY_ARM_L3 = 0x2000,
BODY_HEAD = 0x4000,
BODY_ARM_L = BODY_ARM_L1 | BODY_ARM_L2 | BODY_ARM_L3,
BODY_ARM_R = BODY_ARM_R1 | BODY_ARM_R2 | BODY_ARM_R3,
BODY_LEG_L = BODY_LEG_L1 | BODY_LEG_L2 | BODY_LEG_L3,
BODY_LEG_R = BODY_LEG_R1 | BODY_LEG_R2 | BODY_LEG_R3,
BODY_UPPER = BODY_CHEST | BODY_ARM_L | BODY_ARM_R, // without head
BODY_LOWER = BODY_HIP | BODY_LEG_L | BODY_LEG_R,
BODY_BRAID_MASK = BODY_HEAD | BODY_CHEST | BODY_ARM_L1 | BODY_ARM_L2 | BODY_ARM_R1 | BODY_ARM_R2,
};
bool dozy;
struct Weapon {
enum Type { EMPTY = -1, PISTOLS, SHOTGUN, MAGNUMS, UZIS, MAX };
enum State { IS_HIDDEN, IS_ARMED, IS_FIRING };
struct Anim {
enum Type { NONE, PREPARE, UNHOLSTER, HOLSTER, HOLD, AIM, FIRE };
};
};
Weapon::Type wpnCurrent;
Weapon::Type wpnNext;
Weapon::State wpnState;
int *wpnAmmo;
struct Arm {
Controller *tracking; // tracking target (main target)
Controller *target; // target for shooting
quat rot, rotAbs;
Weapon::Anim::Type anim;
Animation animation;
Arm() : tracking(NULL), target(NULL) {}
} arms[2];
TR::Entity::Type usedKey;
int pickupListCount;
Controller *pickupList[32];
KeyHole *keyHole;
Lightning *lightning;
Texture *environment;
vec2 rotFactor;
float oxygen;
float damageTime;
float hitTime;
int hitDir;
vec3 collisionOffset;
vec3 flowVelocity;
Camera *camera;
float hitTimer;
bool camChanged; // hit key detection to go first person view mode
#ifdef _DEBUG
//uint16 *dbgBoxes;
//int dbgBoxesCount;
#endif
struct Braid {
Lara *lara;
vec3 offset;
Basis *basis;
struct Joint {
vec3 posPrev, pos;
float length;
} *joints;
int jointsCount;
float time;
Braid(Lara *lara, const vec3 &offset) : lara(lara), offset(offset), time(0.0f) {
TR::Level *level = lara->level;
TR::Model *model = getModel();
jointsCount = model->mCount + 1;
joints = new Joint[jointsCount];
basis = new Basis[jointsCount - 1];
Basis basis = getBasis();
basis.translate(offset);
TR::Node *node = (int)model->node < level->nodesDataSize ? (TR::Node*)&level->nodesData[model->node] : NULL;
for (int i = 0; i < jointsCount - 1; i++) {
TR::Node &t = node[min(i, model->mCount - 2)];
joints[i].posPrev = joints[i].pos = basis.pos;
joints[i].length = float(t.z);
basis.translate(vec3(0.0f, 0.0f, -joints[i].length));
}
joints[jointsCount - 1].posPrev = joints[jointsCount - 1].pos = basis.pos;
joints[jointsCount - 1].length = 1.0f;
}
~Braid() {
delete[] joints;
delete[] basis;
}
TR::Model* getModel() {
return &lara->level->models[lara->level->extra.braid];
}
Basis getBasis() {
return lara->getJoint(lara->jointHead);
}
vec3 getPos() {
return getBasis() * offset;
}
void integrate() {
float TIMESTEP = Core::deltaTime;
float ACCEL = 16.0f * GRAVITY * TIMESTEP * TIMESTEP;
float DAMPING = 1.5f;
if (lara->getRoom().flags.water) {
ACCEL *= -0.5f;
DAMPING = 4.0f;
}
DAMPING = 1.0f / (1.0f + DAMPING * TIMESTEP); // Pade approximation
for (int i = 1; i < jointsCount; i++) {
Joint &j = joints[i];
vec3 delta = j.pos - j.posPrev;
delta = delta.normal() * (min(delta.length(), 2048.0f * Core::deltaTime) * DAMPING); // speed limit
j.posPrev = j.pos;
j.pos += delta;
if (lara->stand == STAND_ONWATER) {
if (j.pos.y > lara->pos.y)
j.pos.y += ACCEL;
else
j.pos.y -= ACCEL;
} else
j.pos.y += ACCEL;
}
}
void collide() {
TR::Level *level = lara->level;
const TR::Model *model = lara->getModel();
TR::Level::FloorInfo info;
lara->getFloorInfo(lara->getRoomIndex(), lara->getViewPoint(), info);
for (int j = 1; j < jointsCount; j++)
if (joints[j].pos.y > info.floor)
joints[j].pos.y = info.floor;
#define BRAID_RADIUS 16.0f
lara->updateJoints();
for (int i = 0; i < model->mCount; i++) {
if (!(BODY_BRAID_MASK & (1 << i))) continue;
int offset = level->meshOffsets[model->mStart + i];
TR::Mesh *mesh = (TR::Mesh*)&level->meshes[offset];
vec3 center = lara->joints[i] * mesh->center;
float radiusSq = mesh->radius + BRAID_RADIUS;
radiusSq *= radiusSq;
for (int j = 1; j < jointsCount; j++) {
vec3 dir = joints[j].pos - center;
float len = dir.length2() + EPS;
if (len < radiusSq) {
len = sqrtf(len);
dir *= (mesh->radius + BRAID_RADIUS- len) / len;
joints[j].pos += dir * 0.9f;
}
}
}
#undef BRAID_RADIUS
}
void solve() {
for (int i = 0; i < jointsCount - 1; i++) {
Joint &a = joints[i];
Joint &b = joints[i + 1];
vec3 dir = b.pos - a.pos;
float len = dir.length() + EPS;
dir *= 1.0f / len;
float d = a.length - len;
if (i > 0) {
dir *= d * (0.5f * 1.0f);
a.pos -= dir;
b.pos += dir;
} else
b.pos += dir * (d * 1.0f);
}
}
void update() {
joints[0].pos = getPos();
integrate(); // Verlet integration step
collide(); // check collision with Lara's mesh
for (int i = 0; i < jointsCount; i++) // solve connections (springs)
solve();
vec3 headDir = getBasis().rot * vec3(0.0f, 0.0f, -1.0f);
for (int i = 0; i < jointsCount - 1; i++) {
vec3 d = (joints[i + 1].pos - joints[i].pos).normal();
vec3 r = d.cross(headDir).normal();
vec3 u = d.cross(r).normal();
mat4 m;
m.up() = vec4(u, 0.0f);
m.dir() = vec4(d, 0.0f);
m.right() = vec4(r, 0.0f);
m.offset() = vec4(0.0f, 0.0f, 0.0f, 1.0f);
basis[i].identity();
basis[i].translate(joints[i].pos);
basis[i].rotate(m.getRot());
}
}
void render(MeshBuilder *mesh) {
Core::setBasis(basis, jointsCount - 1);
mesh->renderModel(lara->level->extra.braid);
}
} *braid;
Lara(IGame *game, int entity) : Character(game, entity, LARA_MAX_HEALTH), dozy(false), wpnCurrent(Weapon::EMPTY), wpnNext(Weapon::EMPTY), braid(NULL) {
camera = new Camera(game, this);
hitTimer = 0.0f;
camChanged = false;
if (level->extra.laraSkin > -1)
level->entities[entity].modelIndex = level->extra.laraSkin + 1;
jointChest = 7;
jointHead = 14;
rangeChest = vec4(-0.40f, 0.40f, -0.90f, 0.90f) * PI;
rangeHead = vec4(-0.25f, 0.25f, -0.50f, 0.50f) * PI;
oxygen = LARA_MAX_OXYGEN;
hitDir = -1;
damageTime = LARA_DAMAGE_TIME;
hitTime = 0.0f;
keyHole = NULL;
lightning = NULL;
environment = NULL;
flags.active = 1;
initMeshOverrides();
if (level->isHome()) {
if (level->version & TR::VER_TR1)
meshSwap(1, TR::MODEL_LARA_SPEC, BODY_UPPER | BODY_LOWER);
} else {
if (level->id == TR::LVL_TR2_HOUSE)
wpnSet(Weapon::SHOTGUN);
else
wpnSet(Weapon::PISTOLS);
}
for (int i = 0; i < 2; i++) {
arms[i].rot = quat(0, 0, 0, 1);
arms[i].rotAbs = quat(0, 0, 0, 1);
}
if (level->extra.braid > -1)
braid = new Braid(this, (level->version & (TR::VER_TR2 | TR::VER_TR3)) ? vec3(-2.0f, -16.0f, -48.0f) : vec3(-4.0f, 24.0f, -48.0f));
//reset(14, vec3(40448, 3584, 60928), PI * 0.5f, STAND_ONWATER); // gym (pool)
//reset(0, vec3(74858, 3072, 20795), 0); // level 1 (dart)
//reset(14, vec3(20215, 6656, 52942), PI); // level 1 (bridge)
//reset(20, vec3(8952, 3840, 68071), PI); // level 1 (crystal)
//reset(26, vec3(24475, 6912, 83505), 90 * DEG2RAD); // level 1 (switch timer)
//reset(33, vec3(48229, 4608, 78420), 270 * DEG2RAD); // level 1 (end)
//reset(9, vec3(63008, 0, 37787), 0); // level 2 (switch)
//reset(15, vec3(70067, -256, 29104), -0.68f); // level 2 (pool)
//reset(26, vec3(71980, 1546, 19000), 270 * DEG2RAD); // level 2 (underwater switch)
//reset(61, vec3(27221, -1024, 29205), -PI * 0.5f); // level 2 (blade)
//reset(43, vec3(31400, -2560, 25200), PI); // level 2 (reach)
//reset(16, vec3(60907, 0, 39642), PI * 3 / 2); // level 2 (hang & climb)
//reset(19, vec3(60843, 1024, 30557), PI); // level 2 (block)
//reset(1, vec3(62630, -1280, 19633), 0); // level 2 (dark medikit)
//reset(7, vec3(64108, -512, 16514), -PI * 0.5f); // level 2 (bat trigger)
//reset(15, vec3(70082, -512, 26935), PI * 0.5f); // level 2 (bear)
//reset(63, vec3(31390, -2048, 33472), 0.0f); // level 2 (trap floor)
//reset(61, vec3(21987, -1024, 29144), PI * 3.0f * 0.5f); // level 2 (trap door)
//reset(51, vec3(41015, 3584, 34494), -PI); // level 3a (t-rex)
//reset(5, vec3(38643, -3072, 92370), PI * 0.5f); // level 3a (gears)
//reset(43, vec3(64037, 6656, 48229), PI); // level 3b (movingblock)
//reset(27, vec3(72372, 8704, 46547), PI * 0.5f); // level 3b (spikes)
//reset(5, vec3(73394, 3840, 60758), 0); // level 3b (scion)
//reset(20, vec3(57724, 6656, 61941), 90 * DEG2RAD); // level 3b (boulder)
//reset(18, vec3(34914, 11008, 41315), 90 * DEG2RAD); // level 4 (main hall)
//reset(19, vec3(33368, 19968, 45643), 270 * DEG2RAD); // level 4 (damocles)
//reset(24, vec3(45609, 18176, 41500), 90 * DEG2RAD); // level 4 (thor)
//reset(19, vec3(41418, -3707, 58863), 270 * DEG2RAD); // level 5 (triangle)
//reset(21, vec3(24106, -4352, 52089), 0); // level 6 (flame traps)
//reset(73, vec3(73372, 122, 51687), PI * 0.5f); // level 6 (midas hand)
//reset(64, vec3(36839, -2560, 48769), 270 * DEG2RAD); // level 6 (flipmap effect)
//reset(99, vec3(45562, -3328, 63366), 225 * DEG2RAD); // level 7a (flipmap)
//reset(77, vec3(36943, -4096, 62821), 270 * DEG2RAD); // level 7b (heavy trigger)
//reset(90, vec3(19438, 3840, 78341), 90 * DEG2RAD); // level 7b (statues)
//reset(90, vec3(29000, 3840 - 512, 78341), 90 * DEG2RAD); // level 7b (statues)
//reset(57, vec3(54844, -3328, 53145), 0); // level 8b (bridge switch)
//reset(12, vec3(34236, -2415, 14974), 0); // level 8b (sphinx)
//reset(0, vec3(40913, -1012, 42252), PI); // level 8c
//reset(56, vec3(18541, 512, 52869), PI * 0.5f); // level 8c
//reset(30, vec3(69689, -8448, 34922), 330 * DEG2RAD); // Level 10a (cabin)
//reset(27, vec3(52631, -4352, 57893), 270 * DEG2RAD); // Level 10a (TNT / Cowboy)
//reset(68, vec3(52458, -9984, 93724), 270 * DEG2RAD); // Level 10a (MrT)
//reset(44, vec3(75803, -11008, 21097), 90 * DEG2RAD); // Level 10a (boat)
//reset(47, vec3(50546, -13056, 53783), 270 * DEG2RAD); // Level 10b (trap door slope)
//reset(59, vec3(42907, -13056, 63012), 270 * DEG2RAD); // Level 10b (doppelganger)
//reset(53, vec3(39617, -18385, 48950), 180 * DEG2RAD); // Level 10b (centaur)
//reset(50, vec3(52122, -18688, 47313), 150 * DEG2RAD); // Level 10b (scion holder pickup)
//reset(50, vec3(53703, -18688, 13769), PI); // Level 10c (scion holder)
//reset(19, vec3(35364, -512, 40199), PI * 0.5f); // Level 10c (lava flow)
//reset(9, vec3(69074, -14592, 25192), 0); // Level 10c (trap slam)
//reset(21, vec3(47668, -10752, 32163), 0); // Level 10c (lava emitter)
//reset(10, vec3(90443, 11264 - 256, 114614), PI, STAND_ONWATER); // villa mortal 2
//dbgBoxes = NULL;
if (!level->isCutsceneLevel()) {
if (getRoom().flags.water) {
stand = STAND_UNDERWATER;
animation.setAnim(ANIM_UNDERWATER);
} else
animation.setAnim(ANIM_STAND);
}
}
virtual ~Lara() {
delete camera;
delete braid;
delete environment;
}
bool canSaveGame() {
return health > 0.0f && (state == STATE_STOP || state == STATE_TREAD || state == STATE_SURF_TREAD);
}
virtual bool getSaveData(TR::SaveGame::Entity &data) {
Character::getSaveData(data);
data.extraSize = sizeof(data.extra.lara);
data.extra.lara.velX = velocity.x;
data.extra.lara.velY = velocity.y;
data.extra.lara.velZ = velocity.z;
data.extra.lara.angleX = angle.x;
data.extra.lara.health = health;
data.extra.lara.oxygen = oxygen;
data.extra.lara.stamina = 0.0f;
data.extra.lara.poison = 0.0f;
data.extra.lara.freeze = 0.0f;
data.extra.lara.itemHands = TR::Entity::LARA;
data.extra.lara.itemBack = TR::Entity::SHOTGUN;
data.extra.lara.itemHolster = TR::Entity::PISTOLS;
data.extra.lara.flags.value = 0;
data.extra.lara.flags.burn = 0; // TODO
data.extra.lara.flags.wet = 0; // TODO
return true;
}
virtual void setSaveData(const TR::SaveGame::Entity &data) {
Character::setSaveData(data);
velocity = vec3(data.extra.lara.velX, data.extra.lara.velY, data.extra.lara.velZ);
angle.x = TR::angle(data.extra.lara.angleX);
health = data.extra.lara.health;
oxygen = data.extra.lara.oxygen;
layers[1].mask = layers[2].mask = layers[3].mask = 0;
wpnState = Weapon::IS_HIDDEN;
wpnCurrent = Weapon::EMPTY;
/*
wpnSet(Weapon::Type(data.extra.lara.curWeapon));
if (!data.extra.lara.emptyHands)
wpnDraw(true);
*/
}
int getRoomByPos(const vec3 &pos) {
int x = int(pos.x),
y = int(pos.y),
z = int(pos.z);
for (int i = 0; i < level->roomsCount; i++) {
TR::Room &r = level->rooms[i];
int mx = r.info.x + r.xSectors * 1024;
int mz = r.info.z + r.zSectors * 1024;
if (x >= r.info.x && x < mx && z >= r.info.z && z < mz && y >= r.info.yTop && y < r.info.yBottom)
return i;
}
return TR::NO_ROOM;
}
void reset(int room, const vec3 &pos, float angle, Stand forceStand = STAND_GROUND) {
visibleMask = 0xFFFFFFFF;
health = LARA_MAX_HEALTH;
if (room == TR::NO_ROOM) {
stand = STAND_AIR;
room = getRoomByPos(pos);
}
if (room == TR::NO_ROOM)
return;
if (level->rooms[room].flags.water) {
stand = STAND_UNDERWATER;
animation.setAnim(ANIM_UNDERWATER);
} else {
stand = STAND_GROUND;
animation.setAnim(ANIM_STAND);
}
velocity = vec3(0.0f);
roomIndex = room;
this->pos = pos;
this->angle = vec3(0.0f, angle, 0.0f);
if (forceStand != STAND_GROUND) {
stand = forceStand;
switch (stand) {
case STAND_ONWATER : animation.setAnim(ANIM_TO_ONWATER); break;
case STAND_UNDERWATER : animation.setAnim(ANIM_UNDERWATER); break;
default : ;
}
}
updateZone();
updateLights(false);
camera->changeView(camera->firstPerson);
}
TR::Entity::Type getCurrentWeaponInv() {
switch (wpnCurrent) {
case Weapon::PISTOLS : return TR::Entity::PISTOLS;
case Weapon::SHOTGUN : return TR::Entity::SHOTGUN;
case Weapon::MAGNUMS : return TR::Entity::MAGNUMS;
case Weapon::UZIS : return TR::Entity::UZIS;
default : return TR::Entity::LARA;
}
}
void wpnSet(Weapon::Type wType) {
wpnCurrent = wType;
wpnState = Weapon::IS_FIRING;
TR::Entity::Type invType = getCurrentWeaponInv();
wpnAmmo = game->invCount(invType);
arms[0].animation = arms[1].animation = Animation(level, &level->models[wType == Weapon::SHOTGUN ? TR::MODEL_SHOTGUN : TR::MODEL_PISTOLS]);
wpnSetAnim(arms[0], Weapon::IS_HIDDEN, Weapon::Anim::NONE, 0.0f, 0.0f);
wpnSetAnim(arms[1], Weapon::IS_HIDDEN, Weapon::Anim::NONE, 0.0f, 0.0f);
}
void wpnSetAnim(Arm &arm, Weapon::State wState, Weapon::Anim::Type wAnim, float wAnimTime, float wAnimDir, bool playing = true) {
arm.animation.setAnim(wpnGetAnimIndex(wAnim), 0, wAnim == Weapon::Anim::FIRE);
arm.animation.dir = playing ? wAnimDir : 0.0f;
if (arm.anim != wAnim)
arm.animation.frameIndex = 0xFFFF;
arm.anim = wAnim;
if (wAnimDir > 0.0f)
arm.animation.time = wAnimTime;
else
if (wAnimDir < 0.0f)
arm.animation.time = arm.animation.timeMax + wAnimTime;
arm.animation.updateInfo();
wpnSetState(wState);
}
float wpnGetDamage() {
switch (wpnCurrent) {
case Weapon::PISTOLS : return 1;
case Weapon::SHOTGUN : return 1;
case Weapon::MAGNUMS : return 2;
case Weapon::UZIS : return 1;
default : ;
}
return 0;
}
void wpnSetState(Weapon::State wState) {
if (wpnState == wState || !layers) return;
int mask = 0;
switch (wpnCurrent) {
case Weapon::EMPTY : break;
case Weapon::PISTOLS :
case Weapon::MAGNUMS :
case Weapon::UZIS :
switch (wState) {
case Weapon::IS_HIDDEN : mask = BODY_LEG_L1 | BODY_LEG_R1; break;
case Weapon::IS_ARMED : mask = BODY_ARM_L3 | BODY_ARM_R3; break;
case Weapon::IS_FIRING : mask = BODY_ARM_L3 | BODY_ARM_R3 | BODY_HEAD; break;
}
break;
case Weapon::SHOTGUN :
switch (wState) {
case Weapon::IS_HIDDEN : mask = BODY_CHEST; break;
case Weapon::IS_ARMED : mask = BODY_ARM_L3 | BODY_ARM_R3; break;
case Weapon::IS_FIRING : mask = BODY_ARM_L3 | BODY_ARM_R3 | BODY_HEAD; break;
}
break;
default : ;
}
if (wpnState == Weapon::IS_HIDDEN && wState == Weapon::IS_ARMED) game->playSound(TR::SND_UNHOLSTER, pos, Sound::PAN);
if (wpnState == Weapon::IS_ARMED && wState == Weapon::IS_HIDDEN) game->playSound(TR::SND_HOLSTER, pos, Sound::PAN);
// swap layers
// 0 - body (full)
// 1 - legs (hands, legs)
// 2 - shotgun (hands, chest)
// 3 - angry (head)
// swap weapon parts
if (wpnCurrent != Weapon::SHOTGUN) {
meshSwap(1, level->extra.weapons[wpnCurrent], mask);
// have a shotgun in inventory place it on the back if another weapon is in use
meshSwap(2, level->extra.weapons[Weapon::SHOTGUN], game->invCount(TR::Entity::INV_SHOTGUN) ? BODY_CHEST : 0);
} else {
meshSwap(2, level->extra.weapons[wpnCurrent], mask);
}
// mesh swap to angry Lara's head while firing (from uzis model)
meshSwap(3, level->extra.weapons[Weapon::UZIS], (wState == Weapon::IS_FIRING) ? BODY_HEAD : 0);
wpnState = wState;
}
bool emptyHands() {
return wpnCurrent == Weapon::EMPTY || arms[0].anim == Weapon::Anim::NONE;
}
bool canLookAt() {
return (stand == STAND_GROUND || stand == STAND_SLIDE)
&& state != STATE_REACH
&& state != STATE_PUSH_BLOCK
&& state != STATE_PULL_BLOCK
&& state != STATE_PUSH_PULL_READY
&& state != STATE_PICK_UP;
}
bool canDrawWeapon() {
if (dozy) return true;
return wpnCurrent != Weapon::EMPTY
&& emptyHands()
&& animation.index != ANIM_CLIMB_3
&& animation.index != ANIM_CLIMB_2
&& state != STATE_DEATH
&& state != STATE_HANG
&& state != STATE_REACH
&& state != STATE_TREAD
&& state != STATE_SWIM
&& state != STATE_GLIDE
&& state != STATE_HANG_UP
&& state != STATE_FALL_BACK
&& state != STATE_HANG_LEFT
&& state != STATE_HANG_RIGHT
&& state != STATE_SURF_TREAD
&& state != STATE_SURF_SWIM
&& state != STATE_DIVE
&& state != STATE_PUSH_BLOCK
&& state != STATE_PULL_BLOCK
&& state != STATE_PUSH_PULL_READY
&& state != STATE_PICK_UP
&& state != STATE_SWITCH_DOWN
&& state != STATE_SWITCH_UP
&& state != STATE_USE_KEY
&& state != STATE_USE_PUZZLE
&& state != STATE_UNDERWATER_DEATH
&& state != STATE_SPECIAL
&& state != STATE_SURF_BACK
&& state != STATE_SURF_LEFT
&& state != STATE_SURF_RIGHT
&& state != STATE_SWAN_DIVE
&& state != STATE_FAST_DIVE
&& state != STATE_HANDSTAND
&& state != STATE_WATER_OUT;
}
bool canHitAnim() {
return state == STATE_WALK
|| state == STATE_RUN
|| state == STATE_STOP
|| state == STATE_FAST_BACK
|| state == STATE_TURN_RIGHT
|| state == STATE_TURN_LEFT
|| state == STATE_BACK
|| state == STATE_FAST_TURN
|| state == STATE_STEP_RIGHT
|| state == STATE_STEP_LEFT;
}
bool wpnReady() {
return arms[0].anim != Weapon::Anim::PREPARE && arms[0].anim != Weapon::Anim::UNHOLSTER && arms[0].anim != Weapon::Anim::HOLSTER;
}
void wpnDraw(bool instant = false) {
if (!canDrawWeapon()) return;
if (wpnReady() && emptyHands()) {
if (wpnCurrent != Weapon::SHOTGUN) {
wpnSetAnim(arms[0], wpnState, instant ? Weapon::Anim::AIM : Weapon::Anim::PREPARE, 0.0f, 1.0f);
wpnSetAnim(arms[1], wpnState, instant ? Weapon::Anim::AIM : Weapon::Anim::PREPARE, 0.0f, 1.0f);
} else
wpnSetAnim(arms[0], wpnState, instant ? Weapon::Anim::AIM : Weapon::Anim::UNHOLSTER, 0.0f, 1.0f);
}
}
void wpnHide() {
if (wpnReady() && !emptyHands()) {
if (wpnCurrent != Weapon::SHOTGUN) {
wpnSetAnim(arms[0], wpnState, Weapon::Anim::UNHOLSTER, 0.0f, -1.0f);
wpnSetAnim(arms[1], wpnState, Weapon::Anim::UNHOLSTER, 0.0f, -1.0f);
} else
wpnSetAnim(arms[0], wpnState, Weapon::Anim::HOLSTER, 0.0f, 1.0f);
}
}
void wpnChange(Weapon::Type wType) {
if (wpnCurrent == wType || level->isHome()) {
if (emptyHands())
wpnDraw();
return;
}
wpnNext = wType;
wpnHide();
}
int wpnGetAnimIndex(Weapon::Anim::Type wAnim) {
if (wpnCurrent == Weapon::SHOTGUN) {
switch (wAnim) {
case Weapon::Anim::PREPARE : ASSERT(false); break; // rifle has no prepare animation
case Weapon::Anim::UNHOLSTER : return 1;
case Weapon::Anim::HOLSTER : return 3;
case Weapon::Anim::HOLD :
case Weapon::Anim::AIM : return 0;
case Weapon::Anim::FIRE : return 2;
default : ;
}
} else
switch (wAnim) {
case Weapon::Anim::PREPARE : return 1;
case Weapon::Anim::UNHOLSTER : return 2;
case Weapon::Anim::HOLSTER : ASSERT(false); break; // pistols has no holster animation (it's reversed unholster)
case Weapon::Anim::HOLD :
case Weapon::Anim::AIM : return 0;
case Weapon::Anim::FIRE : return 3;
default : ;
}
return 0;
}
int wpnGetSound() {
switch (wpnCurrent) {
case Weapon::PISTOLS : return TR::SND_PISTOLS_SHOT;
case Weapon::SHOTGUN : return TR::SND_SHOTGUN_SHOT;
case Weapon::MAGNUMS : return TR::SND_MAGNUMS_SHOT;
case Weapon::UZIS : return TR::SND_UZIS_SHOT;
default : return TR::SND_NO;
}
}
void wpnFire() {
bool armShot[2] = { false, false };
for (int i = 0; i < 2; i++) {
Arm &arm = arms[i];
if (arm.anim == Weapon::Anim::FIRE) {
Animation &anim = arm.animation;
//int realFrameIndex = int(arms[i].animation.time * 30.0f / anim->frameRate) % ((anim->frameEnd - anim->frameStart) / anim->frameRate + 1);
if (anim.frameIndex != anim.framePrev) {
if (anim.frameIndex == 0) { //realFrameIndex < arms[i].animation.framePrev) {
if ((input & ACTION) && (!arm.tracking || arm.target))
armShot[i] = true;
else
wpnSetAnim(arm, Weapon::IS_ARMED, Weapon::Anim::AIM, 0.0f, -1.0f, arm.target == NULL);
}
// shotgun reload sound
if (wpnCurrent == Weapon::SHOTGUN) {
if (anim.frameIndex == 10)
game->playSound(TR::SND_SHOTGUN_RELOAD, pos, Sound::PAN);
}
}
}
arm.animation.framePrev = arm.animation.frameIndex;
if (wpnCurrent == Weapon::SHOTGUN) break;
}
if (armShot[0] || armShot[1])
doShot(armShot[0], armShot[1]);
}
void doShot(bool rightHand, bool leftHand) {
if (wpnAmmo && *wpnAmmo != UNLIMITED_AMMO && *wpnAmmo <= 0) { // check for no ammo
game->playSound(TR::SND_EMPTY, pos, Sound::PAN);
wpnChange(Weapon::PISTOLS);
}
int count = wpnCurrent == Weapon::SHOTGUN ? 6 : 2;
float nearDist = 32.0f * 1024.0f;
vec3 nearPos;
int shots = 0;
for (int i = 0; i < count; i++) {
int armIndex;
if (wpnCurrent == Weapon::SHOTGUN) {
if (!rightHand) continue;
armIndex = 0;
} else {
if (!(i ? leftHand : rightHand)) continue;
armIndex = i;
}
Arm *arm = &arms[armIndex];
if (wpnAmmo && *wpnAmmo != UNLIMITED_AMMO) {
if (*wpnAmmo <= 0)
continue;
if (wpnCurrent != Weapon::SHOTGUN)
*wpnAmmo -= 1;
}
shots++;
if (wpnCurrent != Weapon::SHOTGUN)
game->addMuzzleFlash(this, i ? LARA_LGUN_JOINT : LARA_RGUN_JOINT, i ? LARA_LGUN_OFFSET : LARA_RGUN_OFFSET, 1 + camera->cameraIndex);
// TODO: use new trace code
int joint = wpnCurrent == Weapon::SHOTGUN ? 8 : (i ? 11 : 8);
vec3 p = getJoint(joint).pos;
vec3 d = arm->rotAbs * vec3(0, 0, 1);
vec3 t = p + d * (24.0f * 1024.0f) + ((vec3(randf(), randf(), randf()) * 2.0f) - vec3(1.0f)) * 1024.0f;
int room;
vec3 hit = trace(getRoomIndex(), p, t, room, false);
if (arm->target && checkHit(arm->target, p, hit, hit)) {
TR::Entity::Type type = arm->target->getEntity().type;
((Character*)arm->target)->hit(wpnGetDamage(), this);
hit -= d * 64.0f;
if (type != TR::Entity::SCION_TARGET)
game->addEntity(TR::Entity::BLOOD, room, hit);
} else {
hit -= d * 64.0f;
game->addEntity(TR::Entity::RICOCHET, room, hit);
float dist = (hit - p).length();
if (dist < nearDist) {
nearPos = hit;
nearDist = dist;
}
}
}
if (shots) {
game->playSound(wpnGetSound(), pos, Sound::PAN);
game->playSound(TR::SND_RICOCHET, nearPos, Sound::PAN);
if (wpnAmmo && *wpnAmmo != UNLIMITED_AMMO && wpnCurrent == Weapon::SHOTGUN)
*wpnAmmo -= 1;
}
}
void updateWeapon() {
if (level->isCutsceneLevel()) return;
if (wpnNext != Weapon::EMPTY && emptyHands()) {
wpnSet(wpnNext);
wpnDraw();
wpnNext = Weapon::EMPTY;
}
// apply weapon state changes
if (input & WEAPON) {
if (emptyHands())
wpnDraw();
else
wpnHide();
}
if (!emptyHands()) {
bool isRifle = wpnCurrent == Weapon::SHOTGUN;
for (int i = 0; i < 2; i++) {
Arm &arm = arms[i];
if (arm.target || ((input & ACTION) && !arm.tracking)) {
if (arm.anim == Weapon::Anim::HOLD)
wpnSetAnim(arm, wpnState, Weapon::Anim::AIM, 0.0f, 1.0f);
} else
if (arm.anim == Weapon::Anim::AIM)
arm.animation.dir = -1.0f;
if (isRifle) break;
}
for (int i = 0; i < 2; i++)
arms[i].animation.update();
if (isRifle)
animateShotgun();
else
animatePistols();
wpnFire(); // make a shot
}
}
void animatePistols() {
for (int i = 0; i < 2; i++) {
Arm &arm = arms[i];
if (!arm.animation.isEnded) continue;
if (arm.animation.dir >= 0.0f)
switch (arm.anim) {
case Weapon::Anim::PREPARE : wpnSetAnim(arm, Weapon::IS_ARMED, Weapon::Anim::UNHOLSTER, arm.animation.time - arm.animation.timeMax, 1.0f); break;
case Weapon::Anim::UNHOLSTER : wpnSetAnim(arm, Weapon::IS_ARMED, Weapon::Anim::HOLD, 0.0f, 1.0f, false); break;
case Weapon::Anim::AIM :
case Weapon::Anim::FIRE :
if (input & ACTION)
wpnSetAnim(arm, Weapon::IS_FIRING, Weapon::Anim::FIRE, arm.animation.time - arm.animation.timeMax, wpnCurrent == Weapon::UZIS ? 2.0f : 1.0f);
else
wpnSetAnim(arm, Weapon::IS_ARMED, Weapon::Anim::AIM, 0.0f, -1.0f, false);
break;
default : ;
};
if (arm.animation.dir < 0.0f)
switch (arm.anim) {
case Weapon::Anim::PREPARE : wpnSetAnim(arm, Weapon::IS_HIDDEN, Weapon::Anim::NONE, 0.0f, 1.0f, false); break;
case Weapon::Anim::UNHOLSTER : wpnSetAnim(arm, Weapon::IS_HIDDEN, Weapon::Anim::PREPARE, arm.animation.time, -1.0f); break;
case Weapon::Anim::AIM : wpnSetAnim(arm, Weapon::IS_ARMED, Weapon::Anim::HOLD, 0.0f, 1.0f, false); break;
default : ;
};
}
}
void animateShotgun() {
Arm &arm = arms[0];
if (arm.animation.dir >= 0.0f) {
if (arm.animation.isEnded) {
switch (arm.anim) {
case Weapon::Anim::UNHOLSTER : wpnSetAnim(arm, Weapon::IS_ARMED, Weapon::Anim::HOLD, 0.0f, 1.0f, false); break;
case Weapon::Anim::HOLSTER : wpnSetAnim(arm, Weapon::IS_HIDDEN, Weapon::Anim::NONE, 0.0f, 1.0f, false); break;
case Weapon::Anim::AIM :
case Weapon::Anim::FIRE :
if (input & ACTION)
wpnSetAnim(arm, Weapon::IS_FIRING, Weapon::Anim::FIRE, arm.animation.time - arm.animation.timeMax, 1.0f);
else
wpnSetAnim(arm, Weapon::IS_ARMED, Weapon::Anim::AIM, 0.0f, -1.0f, false);
break;
default : ;
}
} else
if (arm.animation.frameIndex != arm.animation.framePrev) {
float delta = arm.animation.time / arm.animation.timeMax;
switch (arm.anim) {
case Weapon::Anim::UNHOLSTER : if (delta >= 0.3f) wpnSetAnim(arm, Weapon::IS_ARMED, arm.anim, arm.animation.time, 1.0f); break;
case Weapon::Anim::HOLSTER : if (delta >= 0.7f) wpnSetAnim(arm, Weapon::IS_HIDDEN, arm.anim, arm.animation.time, 1.0f); break;
default : ;
}
}
} else
if (arm.animation.isEnded && arm.anim == Weapon::Anim::AIM)
wpnSetAnim(arm, Weapon::IS_ARMED, Weapon::Anim::HOLD, 0.0f, 1.0f, false);
}
void updateOverrides() {
int overrideMask = 0;
// head & chest
overrideMask |= BODY_CHEST | BODY_HEAD;
animation.overrides[jointChest] = animation.getJointRot(jointChest);
animation.overrides[jointHead] = animation.getJointRot(jointHead);
/* TODO: shotgun full body animation
if (wpnCurrent == Weapon::SHOTGUN) {
animation.frameA = arms[0].animation.frameA;
animation.frameB = arms[0].animation.frameB;
animation.delta = arms[0].animation.delta;
}
*/
// arms
if (!emptyHands()) {
// right arm
Arm *arm = &arms[0];
animation.overrides[ 8] = arm->animation.getJointRot( 8);
animation.overrides[ 9] = arm->animation.getJointRot( 9);
animation.overrides[10] = arm->animation.getJointRot(10);
// left arm
if (wpnCurrent != Weapon::SHOTGUN) arm = &arms[1];
animation.overrides[11] = arm->animation.getJointRot(11);
animation.overrides[12] = arm->animation.getJointRot(12);
animation.overrides[13] = arm->animation.getJointRot(13);
overrideMask |= (BODY_ARM_R | BODY_ARM_L);
} else
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;
jointsFrame = -1;
}
vec3 getAngle(const vec3 &dir) {
return vec3(atan2f(dir.y, sqrtf(dir.x * dir.x + dir.z * dir.z)) - angle.x, atan2f(dir.x, dir.z) - angle.y + PI, 0.0f);
}
vec3 getAngleAbs(const vec3 &dir) {
return vec3(-atan2f(dir.y, sqrtf(dir.x * dir.x + dir.z * dir.z)), -atan2f(dir.x, dir.z), 0.0f);
}
virtual void lookAt(Controller *target) {
if (health <= 0.0f)
return;
updateOverrides();
Character::lookAt(canLookAt() ? target : NULL);
if (!emptyHands()) {
updateTargets();
if (wpnCurrent == Weapon::SHOTGUN)
aimShotgun();
else
aimPistols();
}
if (target) {
Box box = target->getBoundingBox();
vec3 dir = getJoint(jointHead).pos - box.center();
vec3 angle = getAngle(dir) * RAD2DEG;
camera->setAngle(angle.x, angle.y);
} else {
if (camera->mode == ICamera::MODE_COMBAT) {
camera->setAngle(0, 0);
}
}
}
void aimShotgun() {
quat rot;
Arm &arm = arms[0];
arm.target = aim(arm.target, 14, vec4(-PI * 0.4f, PI * 0.4f, -PI * 0.25f, PI * 0.25f), rot, &arm.rotAbs) ? arm.target : NULL;
}
void aimPistols() {
float speed = 8.0f * Core::deltaTime;
int joints[2] = { 8, 11 };
vec4 ranges[2] = {
vec4(-PI * 0.4f, PI * 0.4f, -PI * 0.2f, PI * 0.5f),
vec4(-PI * 0.4f, PI * 0.4f, -PI * 0.5f, PI * 0.2f),
};
for (int i = 0; i < 2; i++) {
quat rot;
Arm &arm = arms[i];
int j = joints[i];
if (!aim(arm.target, j, ranges[i], rot, &arm.rotAbs)) {
arm.target = arms[i^1].target;
if (!aim(arm.target, j, ranges[i], rot, &arm.rotAbs)) {
rot = quat(0, 0, 0, 1);
arm.target = NULL;
}
}
float t;
if (arm.anim == Weapon::Anim::FIRE)
t = 1.0f;
else if (arm.anim == Weapon::Anim::AIM)
t = arm.animation.time / arm.animation.timeMax;
else
t = 0.0f;
arm.rot = arm.rot.slerp(rot, speed);
animation.overrides[j] = animation.overrides[j].slerp(arm.rot * animation.overrides[j], t);
jointsFrame = -1;
}
}
void updateTargets() {
arms[0].target = arms[1].target = NULL;
viewTarget = NULL;
if (emptyHands() || !wpnReady()) {
arms[0].tracking = arms[1].tracking = NULL;
return;
}
// auto retarget
bool retarget = false;
if (Core::settings.controls[camera->cameraIndex].retarget) {
for (int i = 0; i < 2; i++)
if (!arms[i].tracking || ((Character*)arms[i].tracking)->health <= 0.0f) {
retarget = true;
break;
}
}
int count = wpnCurrent != Weapon::SHOTGUN ? 2 : 1;
if (!(input & ACTION) || retarget) {
getTargets(arms[0].tracking, arms[1].tracking);
if (count == 1)
arms[1].tracking = NULL;
else if (!arms[0].tracking && arms[1].tracking)
arms[0].tracking = arms[1].tracking;
else if (!arms[1].tracking && arms[0].tracking)
arms[1].tracking = arms[0].tracking;
arms[0].target = arms[0].tracking;
arms[1].target = arms[1].tracking;
} else {
if (!arms[0].tracking && !arms[1].tracking)
return;
// flip left and right by relative target direction
if (count > 1) {
int side[2] = { 0, 0 };
vec3 dir = getDir();
dir.y = 0.0f;
for (int i = 0; i < count; i++)
if (arms[i].tracking) {
vec3 v = arms[i].tracking->pos - pos;
v.y = 0;
side[i] = sign(v.cross(dir).y);
}
if (side[0] > 0 && side[1] < 0)
swap(arms[0].tracking, arms[1].tracking);
}
// check occlusion for tracking targets
for (int i = 0; i < count; i++)
if (arms[i].tracking) {
Controller *enemy = (Controller*)arms[i].tracking;
Box box = enemy->getBoundingBox();
vec3 to = box.center();
to.y = box.min.y + (box.max.y - box.min.y) / 3.0f;
vec3 from = pos - vec3(0, 650, 0);
arms[i].target = checkOcclusion(from, to, (to - from).length()) ? arms[i].tracking : NULL;
}
if (count == 1)
arms[1].target = NULL;
else if (!arms[0].target && arms[1].target)
arms[0].target = arms[1].target;
else if (!arms[1].target && arms[0].target)
arms[1].target = arms[0].target;
}
if (arms[0].target && arms[1].target && arms[0].target != arms[1].target) {
viewTarget = NULL; //arms[0].target;
} else if (arms[0].target)
viewTarget = arms[0].target;
else if (arms[1].target)
viewTarget = arms[1].target;
else if (arms[0].tracking)
viewTarget = arms[0].tracking;
else if (arms[1].tracking)
viewTarget = arms[1].tracking;
}
void getTargets(Controller *&target1, Controller *&target2) {
vec3 dir = getDir().normal();
float dist[2] = { TARGET_MAX_DIST, TARGET_MAX_DIST };
target1 = target2 = NULL;
vec3 from = pos - vec3(0, 650, 0);
Controller *c = Controller::first;
do {
if (!c->getEntity().isEnemy())
continue;
Character *enemy = (Character*)c;
if (enemy->health <= 0)
continue;
Box box = enemy->getBoundingBox();
vec3 p = box.center();
p.y = box.min.y + (box.max.y - box.min.y) / 3.0f;
vec3 v = p - pos;
if (dir.dot(v.normal()) <= 0.5f)
continue; // target is out of view range -60..+60 degrees
float d = v.length();
if ((d > dist[0] && d > dist[1]) || !checkOcclusion(from, p, d))
continue;
if (d < dist[0]) {
target2 = target1;
dist[1] = dist[0];
target1 = enemy;
dist[0] = d;
} else if (d < dist[1]) {
target2 = enemy;
dist[1] = d;
}
} while ((c = c->next));
if (!target2 || dist[1] > dist[0] * 4)
target2 = target1;
}
bool checkOcclusion(const vec3 &from, const vec3 &to, float dist) {
int room;
vec3 d = trace(getRoomIndex(), from, to, room, false); // check occlusion
return ((d - from).length() > (dist - 512.0f));
}
bool checkHit(Controller *target, const vec3 &from, const vec3 &to, vec3 &point) {
Box box = target->getBoundingBoxLocal();
mat4 m = target->getMatrix();
float t;
vec3 v = to - from;
if (box.intersect(m, from, v, t)) {
t *= v.length();
v = v.normal();
Sphere spheres[MAX_SPHERES];
int count = target->getSpheres(spheres);
for (int i = 0; i < count; i++) {
float st;
if (spheres[i].intersect(from, v, st)) {
point = from + v * max(t, st);
return true;
}
}
}
return false;
}
virtual void cmdEmpty() {
wpnHide();
}
virtual void cmdOffset(const vec3 &offset) {
Character::cmdOffset(offset);
move();
}
virtual void cmdJump(const vec3 &vel) {
vec3 v = vel;
if (state == STATE_HANG_UP)
v.y = (3.0f - sqrtf(-2.0f * GRAVITY / 30.0f * (collision.info[Collision::FRONT].floor - pos.y + 800.0f - 128.0f)));
Character::cmdJump(v);
}
void drawGun(int right) {
int mask = (right ? BODY_ARM_R3 : BODY_ARM_L3); // unholster
if (layers[1].mask & mask)
mask = (layers[1].mask & ~mask) | (right ? BODY_LEG_R1 : BODY_LEG_L1); // holster
else
mask |= layers[1].mask;
meshSwap(1, level->extra.weapons[wpnCurrent], mask);
}
void doBubbles() {
int count = rand() % 3;
if (!count) return;
game->playSound(TR::SND_BUBBLE, pos, Sound::PAN);
vec3 head = getJoint(jointHead) * vec3(0.0f, 0.0f, 50.0f);
for (int i = 0; i < count; i++)
game->addEntity(TR::Entity::BUBBLE, getRoomIndex(), head, 0);
}
virtual void cmdEffect(int fx) {
switch (fx) {
case TR::Effect::LARA_NORMAL : animation.setAnim(ANIM_STAND); break;
case TR::Effect::LARA_BUBBLES : doBubbles(); break;
case TR::Effect::LARA_HANDSFREE : break;//meshSwap(1, level->extra.weapons[wpnCurrent], BODY_LEG_L1 | BODY_LEG_R1); break;
case TR::Effect::DRAW_RIGHTGUN : drawGun(true); break;
case TR::Effect::DRAW_LEFTGUN : drawGun(false); break;
case TR::Effect::SHOT_RIGHTGUN : game->addMuzzleFlash(this, LARA_RGUN_JOINT, LARA_RGUN_OFFSET, 1 + camera->cameraIndex); break;
case TR::Effect::SHOT_LEFTGUN : game->addMuzzleFlash(this, LARA_LGUN_JOINT, LARA_LGUN_OFFSET, 1 + camera->cameraIndex); break;
case TR::Effect::MESH_SWAP_1 :
case TR::Effect::MESH_SWAP_2 :
case TR::Effect::MESH_SWAP_3 : Character::cmdEffect(fx);
case 26 : break; // TODO TR2 reset_hair
case 32 : break; // TODO TR3 footprint
default : LOG("unknown effect command %d (anim %d)\n", fx, animation.index); ASSERT(false);
}
}
void bakeEnvironment() {
flags.invisible = true;
if (!environment)
environment = new Texture(256, 256, FMT_RGBA, OPT_CUBEMAP | OPT_MIPMAPS | OPT_TARGET);
game->renderEnvironment(getRoomIndex(), pos - vec3(0.0f, 384.0f, 0.0f), &environment);
environment->generateMipMap();
flags.invisible = false;
}
virtual void hit(float damage, Controller *enemy = NULL, TR::HitType hitType = TR::HIT_DEFAULT) {
if (dozy || level->isCutsceneLevel()) return;
if (health <= 0.0f) return;
damageTime = LARA_DAMAGE_TIME;
Character::hit(damage, enemy, hitType);
hitTimer = 0.2f;
switch (hitType) {
case TR::HIT_DART : addBlood(enemy->pos, vec3(0));
case TR::HIT_BLADE : addBloodBlade(); break;
case TR::HIT_SPIKES : addBloodSpikes(); break;
case TR::HIT_SWORD : addBloodBlade(); break;
case TR::HIT_SLAM : addBloodSlam(enemy); break;
case TR::HIT_LIGHTNING : lightning = (Lightning*)enemy; break;
default : ;
}
if (health > 0.0f)
return;
game->stopTrack();
Core::lightColor[1 + 0] = Core::lightColor[1 + 1] = vec4(0, 0, 0, 1);
arms[0].tracking = arms[1].tracking = NULL;
arms[0].target = arms[1].target = NULL;
viewTarget = NULL;
velocity = vec3(0.0f);
animation.overrideMask = 0;
switch (hitType) {
case TR::HIT_FALL : {
animation.setState(STATE_DEATH);
break;
}
case TR::HIT_BOULDER : {
animation.setAnim(ANIM_DEATH_BOULDER);
vec3 v(0.0f);
if (enemy && enemy->getEntity().type == TR::Entity::TRAP_BOULDER) {
angle = enemy->angle;
TR::Level::FloorInfo info;
getFloorInfo(getRoomIndex(), pos, info);
vec3 d = getDir();
v = info.getSlant(d);
float dp = d.dot(v);
if (fabsf(dp) < 0.999)
angle.x = -acosf(dp);
v = ((TrapBoulder*)enemy)->velocity * 2.0f;
}
for (int i = 0; i < 15; i++)
addBlood(256.0f, 512.0f, v);
break;
}
case TR::HIT_SPIKES : {
pos.y = enemy->pos.y;
animation.setAnim(ANIM_DEATH_SPIKES);
for (int i = 0; i < 19; i++)
addBloodSpikes();
break;
}
case TR::HIT_REX : {
pos = enemy->pos;
angle = enemy->angle;
meshSwap(1, TR::MODEL_LARA_SPEC, BODY_UPPER | BODY_LOWER);
meshSwap(2, level->extra.weapons[Weapon::SHOTGUN], 0);
meshSwap(3, level->extra.weapons[Weapon::UZIS], 0);
animation.setAnim(level->models[TR::MODEL_LARA_SPEC].animation + 1);
break;
}
case TR::HIT_MIDAS : {
// generate environment map for reflections
bakeEnvironment();
// set death animation
animation.setAnim(level->models[TR::MODEL_LARA_SPEC].animation + 1);
camera->doCutscene(pos, angle.y);
break;
}
case TR::HIT_GIANT_MUTANT : {
pos = enemy->pos;
angle = enemy->angle;
animation.setAnim(level->models[TR::MODEL_LARA_SPEC].animation);
break;
}
default : ;
}
if (hitType != TR::HIT_LAVA) {
TR::Level::FloorInfo info;
getFloorInfo(getRoomIndex(), pos, info);
if (info.lava && info.floor == pos.y)
hitType = TR::HIT_LAVA;
}
if (hitType == TR::HIT_LAVA) {
for (int i = 0; i < 10; i++)
Flame::add(game, this, int(randf() * 24.0f));
}
};
bool useItem(TR::Entity::Type item) {
if (game->isCutscene()) return false;
switch (item) {
case TR::Entity::INV_PISTOLS : wpnChange(Lara::Weapon::PISTOLS); break;
case TR::Entity::INV_SHOTGUN : wpnChange(Lara::Weapon::SHOTGUN); break;
case TR::Entity::INV_MAGNUMS : wpnChange(Lara::Weapon::MAGNUMS); break;
case TR::Entity::INV_UZIS : wpnChange(Lara::Weapon::UZIS); break;
case TR::Entity::INV_MEDIKIT_SMALL :
case TR::Entity::INV_MEDIKIT_BIG :
damageTime = LARA_DAMAGE_TIME;
health = min(LARA_MAX_HEALTH, health + (item == TR::Entity::INV_MEDIKIT_SMALL ? LARA_MAX_HEALTH / 2 : LARA_MAX_HEALTH));
game->playSound(TR::SND_HEALTH, pos, Sound::PAN);
//TODO: remove medikit item
break;
case TR::Entity::INV_PUZZLE_1 :
case TR::Entity::INV_PUZZLE_2 :
case TR::Entity::INV_PUZZLE_3 :
case TR::Entity::INV_PUZZLE_4 :
case TR::Entity::INV_KEY_1 :
case TR::Entity::INV_KEY_2 :
case TR::Entity::INV_KEY_3 :
case TR::Entity::INV_KEY_4 :
if (usedKey == item)
return false;
usedKey = item;
break;
case TR::Entity::INV_LEADBAR :
for (int i = 0; i < level->entitiesCount; i++) {
const TR::Entity &e = level->entities[i];
if (e.controller && e.type == TR::Entity::MIDAS_HAND) {
MidasHand *controller = (MidasHand*)e.controller;
if (controller->interaction) {
controller->invItem = item;
return false; // remove item from inventory
}
return true;
}
}
return false;
default : return false;
}
return true;
}
bool waterOut() {
// TODO: playSound 36
if (collision.side != Collision::FRONT || pos.y - collision.info[Collision::FRONT].floor > 256 + 128)
return false;
vec3 dst = pos + getDir() * (LARA_RADIUS + 32.0f);
TR::Level::FloorInfo info;
getFloorInfo(getRoomIndex(), pos, info);
int roomAbove = info.roomAbove;
if (roomAbove == TR::NO_ROOM)
return false;
getFloorInfo(roomAbove, dst, info);
int h = int(pos.y - info.floor);
if (h >= 0 && h <= (256 + 128) && (state == STATE_SURF_TREAD || animation.setState(STATE_SURF_TREAD)) && animation.setState(STATE_STOP)) {
alignToWall(LARA_RADIUS);
roomIndex = roomAbove;
pos.y = info.floor;
specular = LARA_WET_SPECULAR;
move();
return true;
}
return false;
}
int goUnderwater() {
angle.x = -PI * 0.25f;
game->waterDrop(pos, 256.0f, 0.2f);
stand = STAND_UNDERWATER;
return animation.setAnim(ANIM_TO_UNDERWATER);
}
bool doPickUp() {
if (!animation.canSetState(STATE_PICK_UP))
return false;
int room = getRoomIndex();
pickupListCount = 0;
for (int i = 0; i < level->entitiesCount; i++) {
TR::Entity &entity = level->entities[i];
if (!entity.controller || !entity.isPickup())
continue;
Controller *controller = (Controller*)entity.controller;
if (controller->getRoomIndex() != room || controller->flags.invisible || !canPickup(controller))
continue;
ASSERT(pickupListCount < COUNT(pickupList));
pickupList[pickupListCount++] = controller;
}
if (pickupListCount > 0) {
state = STATE_PICK_UP;
return true;
}
return false;
}
bool canPickup(Controller *controller) {
TR::Entity::Type type = controller->getEntity().type;
// get limits
TR::Limits::Limit *limit;
switch (type) {
case TR::Entity::SCION_PICKUP_QUALOPEC : limit = &TR::Limits::SCION; break;
case TR::Entity::SCION_PICKUP_HOLDER : limit = &TR::Limits::SCION_HOLDER; break;
default : limit = level->rooms[getRoomIndex()].flags.water ? &TR::Limits::PICKUP_UNDERWATER : &TR::Limits::PICKUP;
}
if (!checkInteraction(controller, limit, true))
return false;
if (stand == Character::STAND_UNDERWATER)
angle.x = -25 * DEG2RAD;
// set new state
switch (type) {
case TR::Entity::SCION_PICKUP_QUALOPEC :
animation.setAnim(level->models[TR::MODEL_LARA_SPEC].animation);
camera->doCutscene(pos, angle.y);
break;
case TR::Entity::SCION_PICKUP_HOLDER :
animation.setAnim(level->models[TR::MODEL_LARA_SPEC].animation);
angle = controller->angle;
pos = controller->pos - vec3(0, -280, LARA_RADIUS + 512).rotateY(angle.y);
camera->doCutscene(pos, angle.y - PI * 0.5f);
break;
default : ;
}
return true;
}
int doTutorial(int track) {
if (level->version == TR::VER_TR1_PC || level->version == TR::VER_TR1_PSX)
switch (track) { // GYM tutorial routine
case 28 : if (level->state.tracks[track].once && state == STATE_UP_JUMP) track = 29; break;
case 37 :
case 41 : if (state != STATE_HANG) return 0; break;
case 42 : if (level->state.tracks[track].once && state == STATE_HANG) track = 43; break;
case 49 : if (state != STATE_SURF_TREAD) return 0; break;
case 50 : // end of GYM
if (level->state.tracks[track].once) {
timer += Core::deltaTime;
if (timer > 3.0f)
game->loadNextLevel();
} else {
if (state != STATE_WATER_OUT)
return 0;
timer = 0.0f;
}
break;
}
return track;
}
bool checkInteraction(Controller *controller, const TR::Limits::Limit *limit, bool action) {
if ((state != STATE_STOP && state != STATE_TREAD && state != STATE_PUSH_PULL_READY) || !action || !emptyHands())
return false;
vec3 tmpAngle = controller->angle;
vec3 ctrlAngle = controller->angle;
if (stand == STAND_UNDERWATER)
ctrlAngle.x = -25 * DEG2RAD;
if (!limit->alignAngle)
ctrlAngle.y = angle.y;
controller->angle = ctrlAngle;
mat4 m = controller->getMatrix();
controller->angle = tmpAngle;
float fx = 0.0f;
if (!limit->alignHoriz)
fx = (m.transpose() * vec4(pos - controller->pos, 0.0f)).x;
vec3 targetPos = controller->pos + (m * vec4(fx, limit->dy, limit->dz, 0.0f)).xyz();
vec3 deltaAbs = pos - targetPos;
vec3 deltaRel = (m.transpose() * vec4(pos - controller->pos, 0.0f)).xyz(); // inverse transform
// set item orientation to hack limits check
if (limit->box.contains(deltaRel)) {
float deltaAngY = shortAngle(angle.y, ctrlAngle.y);
if (stand == STAND_UNDERWATER) {
float deltaAngX = shortAngle(angle.x, ctrlAngle.x);
if (deltaAbs.length() > 64.0f || max(fabs(deltaAngX), fabs(deltaAngY)) > (10.0f * DEG2RAD)) {
pos -= deltaAbs.normal() * min(deltaAbs.length(), Core::deltaTime * 512.0f);
angle.x += sign(deltaAngX) * min(fabsf(deltaAngX), Core::deltaTime * (90.0f * DEG2RAD));
angle.y += sign(deltaAngY) * min(fabsf(deltaAngY), Core::deltaTime * (90.0f * DEG2RAD));
return false;
}
}
if (fabsf(deltaAngY) <= limit->ay * DEG2RAD) {
// align
if (limit->alignAngle)
angle = controller->angle;
else
angle.x = angle.z = 0.0f;
pos = targetPos;
velocity = vec3(0.0f);
speed = 0.0f;
return true;
}
}
return false;
}
void checkTrigger(Controller *controller, bool heavy) {
TR::Level::FloorInfo info;
getFloorInfo(controller->getRoomIndex(), controller->pos, info);
if (getEntity().isLara() && info.lava && info.floor == pos.y) {
hit(LARA_MAX_HEALTH + 1, NULL, TR::HIT_LAVA);
return;
}
if (!info.trigCmdCount) return; // has no trigger
TR::Limits::Limit *limit = NULL;
bool switchIsDown = false;
float timer = info.trigInfo.timer == 1 ? EPS : float(info.trigInfo.timer);
int cmdIndex = 0;
int actionState = state;
switch (info.trigger) {
case TR::Level::Trigger::ACTIVATE : break;
case TR::Level::Trigger::SWITCH : {
Switch *controller = (Switch*)level->entities[info.trigCmd[cmdIndex++].args].controller;
if (controller->flags.state == TR::Entity::asNone) {
limit = state == STATE_STOP ? &TR::Limits::SWITCH : &TR::Limits::SWITCH_UNDERWATER;
if (checkInteraction(controller, limit, Input::state[camera->cameraIndex][cAction])) {
actionState = (controller->state == Switch::STATE_DOWN && stand == STAND_GROUND) ? STATE_SWITCH_UP : STATE_SWITCH_DOWN;
int animIndex;
switch (controller->getEntity().type) {
case TR::Entity::SWITCH_BUTTON : animIndex = ANIM_PUSH_BUTTON; break;
case TR::Entity::SWITCH_BIG : animIndex = controller->state == Switch::STATE_DOWN ? ANIM_SWITCH_BIG_UP : ANIM_SWITCH_BIG_DOWN; break;
default : animIndex = -1;
}
if (animation.setState(actionState, animIndex))
controller->activate();
}
}
if (!controller->setTimer(timer))
return;
switchIsDown = controller->state == Switch::STATE_DOWN;
break;
}
case TR::Level::Trigger::KEY : {
TR::Entity &entity = level->entities[info.trigCmd[cmdIndex++].args];
KeyHole *controller = (KeyHole*)entity.controller;
if (controller->flags.state == TR::Entity::asNone) {
if (controller->flags.active == TR::ACTIVE || state != STATE_STOP)
return;
actionState = entity.isPuzzleHole() ? STATE_USE_PUZZLE : STATE_USE_KEY;
if (!animation.canSetState(actionState))
return;
limit = actionState == STATE_USE_PUZZLE ? &TR::Limits::PUZZLE_HOLE : &TR::Limits::KEY_HOLE;
if (!checkInteraction(controller, limit, isPressed(ACTION) || usedKey != TR::Entity::LARA))
return;
if (usedKey == TR::Entity::LARA) {
if (isPressed(ACTION) && !game->invChooseKey(camera->cameraIndex, entity.type))
game->playSound(TR::SND_NO, pos, Sound::PAN); // no compatible items in inventory
return;
}
if (TR::Entity::convToInv(TR::Entity::getItemForHole(entity.type)) != usedKey) { // check compatibility if user select other
game->playSound(TR::SND_NO, pos, Sound::PAN); // uncompatible item
return;
}
keyHole = controller;
game->invUse(camera->cameraIndex, usedKey);
animation.setState(actionState);
}
if (controller->flags.state != TR::Entity::asInactive)
return;
break;
}
case TR::Level::Trigger::PICKUP : {
Controller *controller = (Controller*)level->entities[info.trigCmd[cmdIndex++].args].controller;
if (!controller->flags.invisible)
return;
break;
}
case TR::Level::Trigger::COMBAT :
if (emptyHands())
return;
break;
case TR::Level::Trigger::PAD :
case TR::Level::Trigger::ANTIPAD :
if (pos.y != info.floor) return;
break;
case TR::Level::Trigger::HEAVY :
if (!heavy) return;
break;
case TR::Level::Trigger::DUMMY :
return;
}
bool needFlip = false;
TR::Effect::Type effect = TR::Effect::NONE;
int cameraIndex = -1;
Controller *cameraTarget = NULL;
while (cmdIndex < info.trigCmdCount) {
TR::FloorData::TriggerCommand &cmd = info.trigCmd[cmdIndex++];
switch (cmd.action) {
case TR::Action::ACTIVATE : {
TR::Entity &e = level->entities[cmd.args];
Controller *controller = (Controller*)e.controller;
ASSERT(controller);
TR::Entity::Flags &flags = controller->flags;
if (flags.once)
break;
controller->timer = timer;
if (info.trigger == TR::Level::Trigger::SWITCH)
flags.active ^= info.trigInfo.mask;
else if (info.trigger == TR::Level::Trigger::ANTIPAD)
flags.active &= ~info.trigInfo.mask;
else
flags.active |= info.trigInfo.mask;
if (flags.active != TR::ACTIVE)
break;
flags.once |= info.trigInfo.once;
controller->activate();
break;
}
case TR::Action::CAMERA_SWITCH : {
TR::FloorData::TriggerCommand &cam = info.trigCmd[cmdIndex++];
if (level->cameras[cmd.args].flags.once)
break;
if (info.trigger == TR::Level::Trigger::COMBAT)
break;
if (info.trigger == TR::Level::Trigger::SWITCH && info.trigInfo.timer && !switchIsDown)
break;
if (info.trigger == TR::Level::Trigger::SWITCH || cmd.args != camera->viewIndexLast) {
level->cameras[cmd.args].flags.once |= cam.once;
camera->setView(cmd.args, cam.timer == 1 ? EPS : float(cam.timer), cam.speed);
}
if (cmd.args == camera->viewIndexLast)
cameraIndex = cmd.args;
break;
}
case TR::Action::FLOW :
applyFlow(level->cameras[cmd.args]);
break;
case TR::Action::FLIP : {
TR::ByteFlags &flip = level->state.flipmaps[cmd.args];
if (flip.once)
break;
if (info.trigger == TR::Level::Trigger::SWITCH)
flip.active ^= info.trigInfo.mask;
else
flip.active |= info.trigInfo.mask;
if (flip.active == TR::ACTIVE)
flip.once |= info.trigInfo.once;
if ((flip.active == TR::ACTIVE) ^ level->state.flags.flipped)
needFlip = true;
break;
}
case TR::Action::FLIP_ON :
if (level->state.flipmaps[cmd.args].active == TR::ACTIVE && !level->state.flags.flipped)
needFlip = true;
break;
case TR::Action::FLIP_OFF :
if (level->state.flipmaps[cmd.args].active == TR::ACTIVE && level->state.flags.flipped)
needFlip = true;
break;
case TR::Action::CAMERA_TARGET :
cameraTarget = (Controller*)level->entities[cmd.args].controller;
break;
case TR::Action::END :
game->loadNextLevel();
break;
case TR::Action::SOUNDTRACK : {
int track = doTutorial(cmd.args);
if (track == 0) break;
// check trigger
TR::ByteFlags &flags = level->state.tracks[track];
if (flags.once)
break;
if (info.trigger == TR::Level::Trigger::SWITCH)
flags.active ^= info.trigInfo.mask;
else if (info.trigger == TR::Level::Trigger::ANTIPAD)
flags.active &= ~info.trigInfo.mask;
else
flags.active |= info.trigInfo.mask;
if ( (flags.active == TR::ACTIVE) || (((level->version & (TR::VER_TR2 | TR::VER_TR3))) && flags.active) ) {
flags.once |= info.trigInfo.once;
game->playTrack(track);
} else
game->stopTrack();
break;
}
case TR::Action::EFFECT :
effect = TR::Effect::Type(cmd.args);
break;
case TR::Action::SECRET :
if (!(level->state.progress.secrets & (1 << cmd.args))) {
level->state.progress.secrets |= 1 << cmd.args;
if (!game->playSound(TR::SND_SECRET, pos))
game->playTrack(TR::TRACK_TR1_SECRET);
}
break;
}
}
if (cameraTarget && (camera->mode == Camera::MODE_STATIC || cameraIndex == -1))
camera->viewTarget = cameraTarget;
if (!cameraTarget && cameraIndex > -1)
camera->viewIndex = cameraIndex;
if (needFlip) {
game->flipMap();
game->setEffect(this, effect);
}
}
virtual Stand getStand() {
if (dozy) return STAND_UNDERWATER;
if (state == STATE_HANG || state == STATE_HANG_LEFT || state == STATE_HANG_RIGHT) {
if (input & ACTION)
return STAND_HANG;
animation.setAnim(ANIM_FALL_HANG);
velocity = vec3(0.0f);
pos.y += 128.0f;
return STAND_AIR;
}
if (state == STATE_HANDSTAND || (state == STATE_HANG_UP && animation.index != ANIM_CLIMB_JUMP))
return STAND_HANG;
if (stand == STAND_ONWATER && state != STATE_STOP) {
if (!getRoom().flags.water && state != STATE_WATER_OUT)
return STAND_AIR;
if (state != STATE_DIVE)
return stand;
}
TR::Level::FloorInfo info;
getFloorInfo(getRoomIndex(), pos, info);
if (getRoom().flags.water) {
if (stand == STAND_UNDERWATER || stand == STAND_ONWATER)
return stand;
wpnHide();
if (stand == STAND_AIR) {
//if (stand != STAND_UNDERWATER && stand != STAND_ONWATER && (state != STATE_FALL && state != STATE_REACH && state != STATE_SWAN_DIVE && state != STATE_FAST_DIVE))
// animation.setAnim(ANIM_FALL_FORTH);
stopScreaming();
return STAND_UNDERWATER;
} else {
pos.y = info.roomCeiling;
return STAND_ONWATER;
}
}
if ((stand == STAND_SLIDE || stand == STAND_GROUND) && (state != STATE_FORWARD_JUMP && state != STATE_BACK_JUMP)) {
if (pos.y + 8 >= info.floor && (abs(info.slantX) > 2 || abs(info.slantZ) > 2)) {
pos.y = info.floor;
if (stand == STAND_GROUND)
slideStart();
return STAND_SLIDE;
}
}
int extra = (stand != STAND_AIR && stand != STAND_SLIDE) ? 256 : 0;
if (pos.y + extra >= info.floor && !(stand == STAND_AIR && velocity.y < 0)) {
if (stand != STAND_GROUND) {
pos.y = info.floor;
// get damage from falling
if (velocity.y > 0.0f) {
stopScreaming();
if (state == STATE_FAST_DIVE && velocity.y > 133.0f) {
hit(health + 1.0f, NULL, TR::HIT_FALL);
} else {
float v = velocity.y - 140.0f;
if (v > 14.0f)
hit(health + 1.0f, NULL, TR::HIT_FALL);
else
if (v > 0.0f)
hit(v * v * LARA_MAX_HEALTH / 196.0f, NULL, TR::HIT_FALL);
}
if (state == STATE_FALL && health > 0.0f)
animation.setAnim(ANIM_LANDING);
}
}
if (stand == STAND_UNDERWATER || stand == STAND_ONWATER)
animation.setAnim(ANIM_STAND);
return STAND_GROUND;
}
return STAND_AIR;
}
void stopScreaming() {
if (velocity.y >= 154.0f)
Sound::stop(TR::SND_SCREAM);
}
virtual int getHeight() {
if (stand == STAND_GROUND || stand == STAND_AIR)
return 768;
return 0;
}
virtual int getStateAir() {
angle.x = 0.0f;
if (velocity.y > 131.0f && state != STATE_SWAN_DIVE && state != STATE_FAST_DIVE)
return STATE_FALL;
if (state == STATE_REACH && getDir().dot(vec3(velocity.x, 0.0f, velocity.z)) < 0)
velocity.x = velocity.z = 0.0f;
if ((state == STATE_REACH || state == STATE_UP_JUMP) && (input & ACTION) && emptyHands()) {
if (state == STATE_REACH && velocity.y < 0.0f)
return state;
Box bounds = animation.getBoundingBox(pos, 0);
vec3 p = vec3(pos.x, bounds.min.y, pos.z);
Collision c = Collision(this, getRoomIndex(), p, getDir() * 128.0f, vec3(0.0f), LARA_RADIUS, angleExt, 0, 0, 0, 0);
if (c.side != Collision::FRONT)
return state;
float floor = c.info[Collision::FRONT].floor;
float ceiling = c.info[Collision::FRONT].ceiling;
float hands = bounds.min.y;
if (fabsf(floor - hands) < 64 && int(floor) != int(ceiling)) {
alignToWall(-LARA_RADIUS);
pos.y = float(floor + LARA_HANG_OFFSET);
stand = STAND_HANG;
if (state == STATE_REACH) {
velocity = vec3(0.0f);
TR::Level::FloorInfo info;
getFloorInfo(getRoomIndex(), pos + getDir() * 256.0f, info);
int h = int(info.ceiling - floor);
return animation.setAnim((h > 0 && h < 400) ? ANIM_HANG_SWING : ANIM_HANG);
} else
return animation.setAnim(ANIM_HANG, -15);
}
}
if (state == STATE_FORWARD_JUMP || state == STATE_FALL_BACK) {
if (emptyHands()) {
if (input & ACTION) return STATE_REACH;
if ((input & (JUMP | FORTH | WALK)) == (JUMP | FORTH | WALK)) return STATE_SWAN_DIVE;
}
} else
if (state != STATE_FALL && state != STATE_FALL_BACK && state != STATE_SWAN_DIVE && state != STATE_FAST_DIVE && state != STATE_REACH && state != STATE_UP_JUMP && state != STATE_BACK_JUMP && state != STATE_LEFT_JUMP && state != STATE_RIGHT_JUMP)
return animation.setAnim(ANIM_FALL_FORTH);// (state == STATE_FAST_BACK || state == STATE_SLIDE_BACK || state == STATE_ROLL_2) ? ANIM_FALL_BACK : ANIM_FALL_FORTH);
if (state == STATE_SWAN_DIVE)
return STATE_FAST_DIVE;
return state;
}
int entityQuadrant(const TR::Entity &entity) {
int ix = int(pos.x) / 1024;
int iz = int(pos.z) / 1024;
int bx = entity.x / 1024;
int bz = entity.z / 1024;
int q = -1;
if (abs(bx - ix) ^ abs(bz - iz)) {
if (bx > ix) q = 1;
if (bx < ix) q = 3;
if (bz > iz) q = 0;
if (bz < iz) q = 2;
}
return q;
}
Block* getBlock() {
for (int i = 0; i < level->entitiesCount; i++) {
TR::Entity &e = level->entities[i];
if (!e.controller || !e.isBlock())
continue;
Block *block = (Block*)e.controller;
float oldAngle = block->angle.y;
block->angle.y = angleQuadrant(angle.y) * (PI * 0.5f);
if (!checkInteraction(block, &TR::Limits::BLOCK, (input & ACTION) != 0)) {
block->angle.y = oldAngle;
continue;
}
return block;
}
return NULL;
}
virtual int getStateGround() {
int res = STATE_STOP;
angle.x = 0.0f;
if ((input == ACTION) && (state == STATE_STOP) && emptyHands() && doPickUp())
return state;
if ((input & (FORTH | ACTION)) == (FORTH | ACTION) && (animation.index == ANIM_STAND || animation.index == ANIM_STAND_NORMAL) && emptyHands() && collision.side == Collision::FRONT) { // TODO: get rid of animation.index
float floor = collision.info[Collision::FRONT].floor;
float ceiling = collision.info[Collision::FRONT].ceiling;
float h = pos.y - floor;
int aIndex = animation.index;
if (floor == ceiling || h < 256)
;// do nothing
else if (h <= 2 * 256 + 128) {
aIndex = ANIM_CLIMB_2;
pos.y = floor + 512.0f;
} else if (h <= 3 * 256 + 128) {
aIndex = ANIM_CLIMB_3;
pos.y = floor + 768.0f;
} else if (h <= 7 * 256 + 128)
aIndex = ANIM_CLIMB_JUMP;
if (aIndex != animation.index) {
alignToWall(-LARA_RADIUS);
return animation.setAnim(aIndex);
}
}
if ( (input & (FORTH | BACK)) == (FORTH | BACK) && (animation.index == ANIM_STAND_NORMAL || state == STATE_RUN) )
return animation.setAnim(ANIM_STAND_ROLL_BEGIN);
// ready to jump
if (state == STATE_COMPRESS) {
float ext = angle.y;
switch (input & (RIGHT | LEFT | FORTH | BACK)) {
case RIGHT : res = STATE_RIGHT_JUMP; ext += PI * 0.5f; break;
case LEFT : res = STATE_LEFT_JUMP; ext -= PI * 0.5f; break;
case FORTH | LEFT :
case FORTH | RIGHT :
case FORTH : res = STATE_FORWARD_JUMP; break;
case BACK : res = STATE_BACK_JUMP; ext += PI; break;
default : res = STATE_UP_JUMP; break;
}
if (res != STATE_UP_JUMP) {
vec3 p = pos;
collision = Collision(this, getRoomIndex(), p, vec3(0.0f), vec3(0.0f), LARA_RADIUS * 2.5f, ext, 0, LARA_HEIGHT, 256 + 128, 0xFFFFFF);
if (collision.side == Collision::FRONT)
res = STATE_UP_JUMP;
}
return res;
}
// jump button is pressed
if (input & JUMP) {
if ((input & FORTH) && state == STATE_FORWARD_JUMP)
return STATE_RUN;
if (state == STATE_RUN)
return STATE_FORWARD_JUMP;
if (animation.index == ANIM_SLIDE_BACK) // TODO: animation index? %)
return STATE_SLIDE_BACK;
return STATE_COMPRESS;
}
// walk button is pressed
if ((input & WALK) && animation.index != ANIM_RUN_START) {
float ext = angle.y;
if (input & FORTH) {
if (state == STATE_BACK)
res = STATE_STOP;
else
res = STATE_WALK;
} else if (input & BACK) {
res = STATE_BACK;
ext += PI;
} else if (input & LEFT) {
res = STATE_STEP_LEFT;
ext -= PI * 0.5f;
} else if (input & RIGHT) {
res = STATE_STEP_RIGHT;
ext += PI * 0.5f;
}
int maxAscent = 256 + 128;
int maxDescent = maxAscent;
if (state == STATE_STEP_LEFT || state == STATE_STEP_RIGHT)
maxAscent = maxDescent = 64;
if (state == STATE_STOP && res != STATE_STOP) {
vec3 p = pos;
collision = Collision(this, getRoomIndex(), p, vec3(0.0f), vec3(0.0f), LARA_RADIUS * 1.1f, ext, 0, LARA_HEIGHT, maxAscent, maxDescent);
if (collision.side == Collision::FRONT)
res = STATE_STOP;
}
return res;
}
if ((input & ACTION) && emptyHands()) {
if (state == STATE_PUSH_PULL_READY && (input & (FORTH | BACK))) {
int pushState = (input & FORTH) ? STATE_PUSH_BLOCK : STATE_PULL_BLOCK;
Block *block = getBlock();
if (block && animation.canSetState(pushState) && block->doMove((input & FORTH) != 0))
return pushState;
}
if (state == STATE_PUSH_PULL_READY || getBlock())
return STATE_PUSH_PULL_READY;
}
// only dpad buttons pressed
if (input & FORTH)
res = STATE_RUN;
else if (input & BACK)
res = STATE_FAST_BACK;
else if (input & (LEFT | RIGHT)) {
if (state == STATE_FAST_TURN)
return state;
if (input & LEFT) return (state == STATE_TURN_LEFT && animation.prev == animation.index) ? STATE_FAST_TURN : STATE_TURN_LEFT;
if (input & RIGHT) return (state == STATE_TURN_RIGHT && animation.prev == animation.index) ? STATE_FAST_TURN : STATE_TURN_RIGHT;
}
if (state == STATE_STOP && res != STATE_STOP) {
float ext = angle.y + (res == STATE_RUN ? 0.0f : PI);
vec3 p = pos;
collision = Collision(this, getRoomIndex(), p, vec3(0.0f), vec3(0.0f), LARA_RADIUS * 1.1f, ext, 0, LARA_HEIGHT, 256 + 128, 0xFFFFFF);
if (collision.side == Collision::FRONT)
res = STATE_STOP;
}
return res;
}
void slideStart() {
if (state != STATE_SLIDE && state != STATE_SLIDE_BACK) {
TR::Level::FloorInfo info;
getFloorInfo(getRoomIndex(), pos, info);
int sx = abs(info.slantX), sz = abs(info.slantZ);
// get direction
float dir;
if (sx > sz)
dir = info.slantX > 0 ? 3.0f : 1.0f;
else
dir = info.slantZ > 0 ? 2.0f : 0.0f;
dir *= PI * 0.5f;
int aIndex = ANIM_SLIDE_FORTH;
if (fabsf(shortAngle(dir, angle.y)) > PI * 0.5f) {
aIndex = ANIM_SLIDE_BACK;
dir += PI;
}
angle.y = dir;
animation.setAnim(aIndex);
}
}
virtual int getStateSlide() {
if (input & JUMP)
return state == STATE_SLIDE ? STATE_FORWARD_JUMP : STATE_BACK_JUMP;
// TODO: update slide direction
return state;
}
virtual int getStateHang() {
if (input & LEFT) return STATE_HANG_LEFT;
if (input & RIGHT) return STATE_HANG_RIGHT;
if (input & FORTH) {
// possibility check
TR::Level::FloorInfo info;
getFloorInfo(getRoomIndex(), pos + getDir() * (LARA_RADIUS + 2.0f), info);
if (info.floor - info.ceiling >= LARA_HEIGHT)
return (input & WALK) ? STATE_HANDSTAND : STATE_HANG_UP;
}
return STATE_HANG;
}
virtual int getStateUnderwater() {
if ((input == ACTION) && (state == STATE_TREAD) && emptyHands() && doPickUp())
return state;
if (state == STATE_FORWARD_JUMP || state == STATE_UP_JUMP || state == STATE_BACK_JUMP || state == STATE_LEFT_JUMP || state == STATE_RIGHT_JUMP || state == STATE_FALL || state == STATE_REACH || state == STATE_SLIDE || state == STATE_SLIDE_BACK) {
game->waterDrop(pos, 256.0f, 0.2f);
if (level->extra.waterSplash > -1)
game->addEntity(TR::Entity::WATER_SPLASH, getRoomIndex(), pos);
pos.y += 100.0f;
angle.x = -45.0f * DEG2RAD;
return animation.setAnim(ANIM_WATER_FALL); // TODO: wronng animation
}
if (state == STATE_SWAN_DIVE || state == STATE_FAST_DIVE) {
angle.x = -PI * 0.5f;
game->waterDrop(pos, 128.0f, 0.2f);
if (level->extra.waterSplash > -1)
game->addEntity(TR::Entity::WATER_SPLASH, getRoomIndex(), pos);
return STATE_DIVE;
}
if (input & JUMP) return STATE_SWIM;
if (state == STATE_GLIDE && speed < LARA_SWIM_SPEED * 2.0f / 3.0f)
return STATE_TREAD;
return STATE_GLIDE;
}
virtual int getStateOnwater() {
angle.x = 0.0f;
if (state == STATE_WATER_OUT) return state;
if (state != STATE_SURF_TREAD && state != STATE_SURF_LEFT && state != STATE_SURF_RIGHT && state != STATE_SURF_SWIM && state != STATE_SURF_BACK && state != STATE_STOP) {
game->waterDrop(pos, 128.0f, 0.2f);
specular = LARA_WET_SPECULAR;
return animation.setAnim(ANIM_TO_ONWATER);
}
if (state == STATE_SURF_TREAD) {
if (animation.isFrameActive(0))
game->waterDrop(getJoint(jointHead).pos, 96.0f, 0.03f);
} else {
if (animation.frameIndex % 4 == 0)
game->waterDrop(getJoint(jointHead).pos, 96.0f, 0.02f);
}
if (input & FORTH) {
if (input & JUMP)
return goUnderwater();
if ((input & ACTION) && waterOut()) {
game->waterDrop(pos, 128.0f, 0.2f);
return state;
}
return STATE_SURF_SWIM;
}
if (input & BACK) return STATE_SURF_BACK;
if (input & WALK) {
if (input & LEFT) return STATE_SURF_LEFT;
if (input & RIGHT) return STATE_SURF_RIGHT;
}
return STATE_SURF_TREAD;
}
virtual int getStateDeath() {
velocity = vec3(0.0f);
return (stand == STAND_UNDERWATER || stand == STAND_ONWATER) ? STATE_UNDERWATER_DEATH : (state == STATE_MIDAS_DEATH ? STATE_MIDAS_DEATH : STATE_DEATH);
}
virtual int getStateDefault() {
if (state == STATE_DIVE || (state == STATE_RUN && (input & JUMP)) ) return state;
switch (stand) {
case STAND_GROUND : return STATE_STOP;
case STAND_HANG : return STATE_HANG;
case STAND_ONWATER : return STATE_SURF_TREAD;
case STAND_UNDERWATER : return STATE_TREAD;
default : ;
}
return STATE_FALL;
}
virtual void updateState() {
Character::updateState();
if (camera->mode != ICamera::MODE_FOLLOW)
return;
camera->centerView = false;
switch (state) {
case STATE_WATER_OUT :
camera->centerView = true;
break;
case STATE_DEATH :
case STATE_UNDERWATER_DEATH :
camera->centerView = true;
break;
case STATE_REACH :
camera->setAngle(0, 85);
break;
case STATE_BACK_JUMP :
camera->setAngle(0, 135);
break;
case STATE_SURF_TREAD :
case STATE_SURF_SWIM :
case STATE_SURF_BACK :
case STATE_SURF_LEFT :
case STATE_SURF_RIGHT :
camera->setAngle(-22, 0);
break;
case STATE_SLIDE :
case STATE_SLIDE_BACK :
camera->setAngle(-45, 0);
break;
case STATE_HANG :
case STATE_HANG_LEFT :
case STATE_HANG_RIGHT :
camera->setAngle(-60, 0);
break;
case STATE_PUSH_BLOCK :
case STATE_PULL_BLOCK :
camera->setAngle(-25, 35);
camera->centerView = true;
break;
case STATE_PUSH_PULL_READY :
camera->setAngle(0, 75);
break;
case STATE_PICK_UP :
camera->setAngle(-15, -130);
break;
case STATE_SWITCH_DOWN :
case STATE_SWITCH_UP :
camera->setAngle(-25, 80);
break;
case STATE_USE_KEY :
case STATE_USE_PUZZLE :
camera->setAngle(-25, -80);
break;
case STATE_SPECIAL :
camera->setAngle(-25, 170);
camera->centerView = true;
break;
default :
camera->setAngle(0, 0);
}
}
virtual int getInput() { // TODO: updateInput
if (level->isCutsceneLevel()) return 0;
input = 0;
int pid = camera->cameraIndex;
if (!dozy && ((Input::state[pid][cAction] && Input::state[pid][cJump] && Input::state[pid][cLook] && Input::state[pid][cDash]) || Input::down[ikO])) {
dozy = true;
health = LARA_MAX_HEALTH;
oxygen = LARA_MAX_OXYGEN;
reset(getRoomIndex(), pos - vec3(0, 512, 0), angle.y, STAND_UNDERWATER);
return input;
}
if (dozy && Input::state[pid][cWalk]) {
dozy = false;
return input;
}
input = Character::getInput();
if (input & DEATH) return input;
if (Input::state[pid][cLook]) input |= LOOK;
if (!(input & LOOK)) {
if (Input::state[pid][cUp]) input |= FORTH;
if (Input::state[pid][cRight]) input |= RIGHT;
if (Input::state[pid][cDown]) input |= BACK;
if (Input::state[pid][cLeft]) input |= LEFT;
}
if (Input::state[pid][cRoll]) input = FORTH | BACK;
//if (Input::state[pid][cStepRight]) input = WALK | RIGHT;
//if (Input::state[pid][cStepLeft]) input = WALK | LEFT;
if (Input::state[pid][cJump]) input |= JUMP;
if (Input::state[pid][cWalk]) input |= WALK;
if (Input::state[pid][cAction]) input |= ACTION;
if (Input::state[pid][cWeapon]) input |= WEAPON;
// scion debug (TODO: remove)
if (Input::down[ikP]) {
switch (level->id) {
case TR::LVL_TR1_GYM :
reset(14, vec3(40448, 3584, 60928), PI * 0.5f, STAND_ONWATER); // gym (pool)
break;
case TR::LVL_TR1_2 :
reset(61, vec3(21987, -1024, 29144), PI * 3.0f * 0.5f); // level 2 (trap door)
break;
case TR::LVL_TR1_3A :
reset(51, vec3(41015, 3584, 34494), -PI); // level 3a (t-rex)
break;
case TR::LVL_TR1_3B :
reset(5, vec3(73394, 3840, 60758), 0); // level 3b (scion)
break;
case TR::LVL_TR1_4 :
reset(18, vec3(34914, 11008, 41315), 90 * DEG2RAD); // main hall
break;
case TR::LVL_TR1_6 :
reset(73, vec3(73372, 122, 51687), PI * 0.5f); // level 6 (midas hand)
break;
case TR::LVL_TR1_7A :
reset(99, vec3(45562, -3328, 63366), 225 * DEG2RAD); // level 7a (flipmap)
break;
case TR::LVL_TR1_7B :
// reset(77, vec3(36943, -4096, 62821), 270 * DEG2RAD); // level 7b (heavy trigger)
reset(49, vec3(45596, -6144, 71579), 0); // level 7b (water trigger)
break;
case TR::LVL_TR2_WALL :
//reset(44, vec3(62976, 1536, 23040), 0);
reset(44, vec3(62976, 1536, 23040), 0);
break;
case TR::LVL_TR2_PLATFORM :
reset(16, vec3(53029, -5120, 77359), 0);
break;
case TR::LVL_TR3_TEMPLE :
reset(204, vec3(40562, 3584, 58694), 0);
break;
default : game->playSound(TR::SND_NO, pos, Sound::PAN);
}
}
// analog control
rotFactor = vec2(1.0f);
if (input & LOOK)
return input;
Input::Joystick &joy = Input::joy[Core::settings.controls[pid].joyIndex];
if (!((state == STATE_STOP || state == STATE_SURF_TREAD || state == STATE_HANG) && fabsf(joy.L.x) < 0.5f && fabsf(joy.L.y) < 0.5f)) {
bool moving = state == STATE_RUN || state == STATE_WALK || state == STATE_BACK || state == STATE_FAST_BACK || state == STATE_SURF_SWIM || state == STATE_SURF_BACK || state == STATE_FORWARD_JUMP;
if (!moving) {
if (fabsf(joy.L.x) < fabsf(joy.L.y))
joy.L.x = 0.0f;
else
joy.L.y = 0.0f;
}
if (joy.L.x != 0.0f) {
input |= (joy.L.x < 0.0f) ? LEFT : RIGHT;
if (moving || stand == STAND_UNDERWATER || stand == STAND_ONWATER)
rotFactor.y = min(fabsf(joy.L.x) / 0.9f, 1.0f);
}
if (joy.L.y != 0.0f) {
input |= (joy.L.y < 0.0f) ? FORTH : BACK;
if (stand == STAND_UNDERWATER)
rotFactor.x = min(fabsf(joy.L.y) / 0.9f, 1.0f);
}
}
// VR control
if (Core::settings.detail.stereo == Core::Settings::STEREO_VR && camera->firstPerson && canFreeRotate()) {
if (!(input & WALK)) {
input &= ~(LEFT | RIGHT);
}
vec3 ang = getAngleAbs(Input::hmd.head.dir().xyz());
angle.y = ang.y;
// rotFactor = vec2(1.0f);
// ang.y = shortAngle(angle.y, ang.y);
// if (fabsf(ang.y) > 5 * DEG2RAD)
// input |= ang.y < 0.0f ? LEFT : RIGHT;
if (stand == STAND_UNDERWATER) {
input &= ~(FORTH | BACK);
angle.x = ang.x;
// ang.x = shortAngle(angle.x, ang.x);
// if (fabsf(ang.x) > 5 * DEG2RAD)
// input |= ang.x < 0.0f ? FORTH : BACK;
}
}
return input;
}
virtual bool useHeadAnimation() {
return state == STATE_WATER_OUT
|| state == STATE_DEATH
|| state == STATE_UNDERWATER_DEATH
|| state == STATE_HANG
|| state == STATE_HANG_UP
|| state == STATE_HANG_LEFT
|| state == STATE_HANG_RIGHT
|| state == STATE_PUSH_BLOCK
|| state == STATE_PULL_BLOCK
|| state == STATE_PUSH_PULL_READY
|| state == STATE_PICK_UP
|| state == STATE_SWITCH_DOWN
|| state == STATE_SWITCH_UP
|| state == STATE_USE_KEY
|| state == STATE_USE_PUZZLE
|| state == STATE_SPECIAL
|| state == STATE_REACH
|| state == STATE_SWAN_DIVE
|| state == STATE_FAST_DIVE
|| state == STATE_HANDSTAND
|| state == STATE_ROLL_1
|| state == STATE_ROLL_2
|| state == STATE_MIDAS_USE
|| state == STATE_MIDAS_DEATH
// make me sick!
// || state == STATE_BACK_JUMP
// || state == STATE_LEFT_JUMP
// || state == STATE_RIGHT_JUMP
|| animation.index == ANIM_CLIMB_2
|| animation.index == ANIM_CLIMB_3
|| animation.index == ANIM_CLIMB_JUMP;
}
bool canFreeRotate() {
return !(useHeadAnimation() || state == STATE_SLIDE || state == STATE_SLIDE_BACK);
}
virtual void doCustomCommand(int curFrame, int prevFrame) {
switch (state) {
case STATE_PICK_UP : {
int pickupFrame = stand == STAND_GROUND ? PICKUP_FRAME_GROUND : PICKUP_FRAME_UNDERWATER;
if (animation.isFrameActive(pickupFrame)) {
for (int i = 0; i < pickupListCount; i++) {
if (pickupList[i]->getEntity().type == TR::Entity::SCION_PICKUP_HOLDER)
continue;
pickupList[i]->deactivate();
pickupList[i]->flags.invisible = true;
game->invAdd(pickupList[i]->getEntity().type, 1);
}
pickupListCount = 0;
}
break;
}
case STATE_USE_KEY :
case STATE_USE_PUZZLE : {
if (keyHole && animation.isFrameActive(state == STATE_USE_PUZZLE ? PUZZLE_FRAME : KEY_FRAME)) {
keyHole->activate();
keyHole = NULL;
}
break;
}
}
}
virtual void update() {
if ((Input::state[camera->cameraIndex][cLook]) && (Input::state[camera->cameraIndex][cAction])) {
if (!camChanged) {
camera->changeView(!camera->firstPerson);
camChanged = true;
}
} else
camChanged = false;
if (level->isCutsceneLevel()) {
updateAnimation(true);
updateLights();
if (fixRoomIndex() && braid)
braid->update();
} else {
Character::update();
if (braid)
braid->update();
}
camera->update();
if (hitTimer > 0.0f) {
hitTimer -= Core::deltaTime;
if (hitTimer > 0.0f)
Input::setJoyVibration(camera->cameraIndex, 0.5f, 0.5f);
else
Input::setJoyVibration(camera->cameraIndex, 0, 0);
}
if (level->isCutsceneLevel())
return;
if (damageTime > 0.0f)
damageTime = max(0.0f, damageTime - Core::deltaTime);
if (stand == STAND_UNDERWATER && !dozy) {
if (oxygen > 0.0f)
oxygen -= Core::deltaTime;
else
hit(Core::deltaTime * 150.0f);
} else
if (oxygen < LARA_MAX_OXYGEN && health > 0.0f)
oxygen = min(LARA_MAX_OXYGEN, oxygen += Core::deltaTime * 10.0f);
usedKey = TR::Entity::LARA;
if (camera->mode != Camera::MODE_CUTSCENE && camera->mode != Camera::MODE_STATIC)
camera->mode = (emptyHands() || health <= 0.0f) ? Camera::MODE_FOLLOW : Camera::MODE_COMBAT;
}
virtual void updateAnimation(bool commands) {
Controller::updateAnimation(commands);
updateWeapon();
if (stand == STAND_UNDERWATER)
specular = LARA_MIN_SPECULAR;
else
if (specular > LARA_MIN_SPECULAR)
specular = max(LARA_MIN_SPECULAR, specular - LARA_WET_TIMER * Core::deltaTime);
if (state == STATE_MIDAS_DEATH || state == STATE_MIDAS_USE) {
uint32 sparklesMask = getMidasMask();
if (state == STATE_MIDAS_DEATH)
visibleMask = sparklesMask ^ 0xFFFFFFFF;
timer += Core::deltaTime;
if (timer >= 1.0f / 30.0f) {
timer -= 1.0f / 30.0f;
addSparks(sparklesMask);
}
}
}
virtual void updateVelocity() {
flowVelocity = vec3(0);
if (!(input & DEATH) && !level->isCutsceneLevel())
checkTrigger(this, false);
// get turning angle
float w = (input & LEFT) ? -1.0f : ((input & RIGHT) ? 1.0f : 0.0f);
if (state == STATE_SWIM || state == STATE_GLIDE)
w *= TURN_WATER_FAST;
else if (state == STATE_TREAD || state == STATE_SURF_TREAD || state == STATE_SURF_SWIM || state == STATE_SURF_BACK)
w *= TURN_WATER_FAST;
else if (state == STATE_RUN) {
if (Core::settings.detail.stereo == Core::Settings::STEREO_VR)
w *= TURN_FAST;
else
w *= sign(w) != sign(tilt) ? 0.0f : w * TURN_FAST * tilt / LARA_TILT_MAX;
} else if (state == STATE_FAST_TURN)
w *= TURN_FAST;
else if (state == STATE_FAST_BACK)
w *= TURN_FAST_BACK;
else if (state == STATE_TURN_LEFT || state == STATE_TURN_RIGHT || state == STATE_WALK)
w *= TURN_NORMAL;
else if (state == STATE_FORWARD_JUMP || state == STATE_BACK)
w *= TURN_SLOW;
else
w = 0.0f;
if (w != 0.0f)
rotateY(w * rotFactor.y * Core::deltaTime);
// pitch (underwater only)
if (stand == STAND_UNDERWATER && (input & (FORTH | BACK)))
rotateX(((input & FORTH) ? -TURN_WATER_SLOW : TURN_WATER_SLOW) * rotFactor.x * Core::deltaTime);
// get animation direction
angleExt = angle.y;
switch (state) {
case STATE_BACK :
case STATE_SURF_BACK :
case STATE_BACK_JUMP :
case STATE_FAST_BACK :
case STATE_SLIDE_BACK :
case STATE_ROLL_1 :
angleExt += PI;
break;
case STATE_LEFT_JUMP :
case STATE_STEP_LEFT :
case STATE_SURF_LEFT :
case STATE_HANG_LEFT :
angleExt -= PI * 0.5f;
break;
case STATE_RIGHT_JUMP :
case STATE_STEP_RIGHT :
case STATE_SURF_RIGHT :
case STATE_HANG_RIGHT :
angleExt += PI * 0.5f;
break;
}
switch (stand) {
case STAND_AIR :
applyGravity(velocity.y);
if (velocity.y >= 154.0f && state == STATE_FALL)
game->playSound(TR::SND_SCREAM, pos, Sound::PAN);
/*
if (state == STATE_FALL || state == STATE_FAST_DIVE) {
velocity.x *= 0.95 * Core::deltaTime;
velocity.z *= 0.95 * Core::deltaTime;
}
*/
break;
case STAND_GROUND :
case STAND_SLIDE :
case STAND_HANG :
case STAND_ONWATER : {
switch (state) {
case STATE_SURF_SWIM :
case STATE_SURF_BACK :
case STATE_SURF_LEFT :
case STATE_SURF_RIGHT :
speed = min(speed + 30.0f * LARA_WATER_ACCEL * Core::deltaTime, LARA_SURF_SPEED);
break;
default :
speed = animation.getSpeed();
}
if (stand == STAND_ONWATER) {
velocity.x = sinf(angleExt) * speed;
velocity.z = cosf(angleExt) * speed;
velocity.y = 0.0f;
} else {
TR::Level::FloorInfo info;
if (stand == STAND_HANG) {
vec3 p = pos + getDir() * (LARA_RADIUS + 2.0f);
getFloorInfo(getRoomIndex(), p, info);
if (info.roomAbove != TR::NO_ROOM && info.floor >= pos.y - LARA_HANG_OFFSET)
getFloorInfo(info.roomAbove, p, info);
} else
getFloorInfo(getRoomIndex(), pos, info);
vec3 v(sinf(angleExt), 0.0f, cosf(angleExt));
velocity = info.getSlant(v) * speed;
}
break;
}
case STAND_UNDERWATER : {
if (animation.index == ANIM_TO_UNDERWATER)
speed = 15.0f;
if (state == STATE_SWIM)
speed = min(speed + 30.0f * LARA_WATER_ACCEL * Core::deltaTime, LARA_SWIM_SPEED);
if (state == STATE_TREAD || state == STATE_GLIDE)
speed = max(speed - 30.0f * LARA_SWIM_FRICTION * Core::deltaTime, 0.0f);
velocity = vec3(angle.x, angle.y) * speed;
// TODO: apply flow velocity
break;
}
}
if (state == STATE_DEATH || state == STATE_UNDERWATER_DEATH)
velocity.x = velocity.z = 0.0f;
}
virtual void updatePosition() { // TODO: sphere / bbox collision
if (level->isCutsceneLevel())
return;
// tilt control
vec2 vTilt(LARA_TILT_SPEED * Core::deltaTime, LARA_TILT_MAX);
if (stand == STAND_UNDERWATER)
vTilt *= 2.0f;
vTilt *= rotFactor.y;
bool VR = (Core::settings.detail.stereo == Core::Settings::STEREO_VR) && camera->firstPerson;
updateTilt((state == STATE_RUN || stand == STAND_UNDERWATER) && !VR, vTilt.x, vTilt.y);
collisionOffset = vec3(0.0f);
if (checkCollisions() || (velocity + flowVelocity + collisionOffset).length2() >= 1.0f) // TODO: stop & smash anim
move();
}
virtual vec3 getPos() {
return level->isCutsceneLevel() ? getViewPoint() : pos;
}
bool checkCollisions() {
// check static objects (TODO: check linked rooms?)
const TR::Room &room = getRoom();
Box box(pos - vec3(LARA_RADIUS, LARA_HEIGHT, LARA_RADIUS), pos + vec3(LARA_RADIUS, 0.0f, LARA_RADIUS));
for (int i = 0; i < room.meshesCount; i++) {
TR::Room::Mesh &m = room.meshes[i];
TR::StaticMesh &sm = level->staticMeshes[m.meshIndex];
if (sm.flags != 2) continue;
Box meshBox;
sm.getBox(true, m.rotation, meshBox);
meshBox.translate(vec3(float(m.x), float(m.y), float(m.z)));
if (!box.intersect(meshBox)) continue;
collisionOffset += meshBox.pushOut2D(box);
}
// check enemies & doors
for (int i = 0; i < level->entitiesCount; i++) {
const TR::Entity &e = level->entities[i];
if (!e.controller || !e.isCollider()) continue;
Controller *controller = (Controller*)e.controller;
if (e.isEnemy()) {
if (e.type != TR::Entity::ENEMY_REX && (controller->flags.active != TR::ACTIVE || ((Character*)controller)->health <= 0)) continue;
} else {
// fast distance check for object
if (e.type != TR::Entity::HAMMER_HANDLE && e.type != TR::Entity::HAMMER_BLOCK && e.type != TR::Entity::SCION_HOLDER)
if (fabsf(pos.x - controller->pos.x) > 1024 || fabsf(pos.z - controller->pos.z) > 1024 || fabsf(pos.y - controller->pos.y) > 2048) continue;
}
vec3 dir = pos - vec3(0.0f, 128.0f, 0.0f) - controller->pos;
vec3 p = dir.rotateY(controller->angle.y);
Box box = controller->getBoundingBoxLocal();
box.expand(vec3(LARA_RADIUS + 50.0f, 0.0f, LARA_RADIUS + 50.0f));
box.max.y += 768;
if (!box.contains(p)) // TODO: Box vs Box or check Lara's head point? (check thor hammer handle)
continue;
if (e.isEnemy()) { // enemy collision
if (!collide(controller, false))
continue;
// velocity.x = velocity.y = 0.0f;
} else { // door collision
p += box.pushOut2D(p);
p = (p.rotateY(-controller->angle.y) + controller->pos) - pos;
collisionOffset += vec3(p.x, 0.0f, p.z);
}
if (e.type == TR::Entity::ENEMY_REX && ((Character*)controller)->health <= 0)
return true;
if (!e.isEnemy() || e.type == TR::Entity::ENEMY_BAT)
return true;
if (canHitAnim()) { // TODO: check enemy type and health here
// get hit dir
if (hitDir == -1) {
if (health > 0)
game->playSound(TR::SND_HIT, pos, Sound::PAN);
hitTime = 0.0f;
}
if (level->version & TR::VER_TR1) // TODO: check hit animation indices for TR2 and TR3
hitDir = angleQuadrant(dir.rotateY(angle.y + PI * 0.5f).angleY());
return true;
}
};
if (lightning && lightning->flash && !lightning->armed) {
if (hitDir == -1)
hitTime = 0.0f;
hitDir = int(randf() * 4);
} else {
hitDir = -1;
lightning = NULL;
}
return false;
}
void move() {
vec3 vel = (velocity + flowVelocity) * Core::deltaTime * 30.0f + collisionOffset;
vec3 opos(pos), offset(0.0f);
float radius = stand == STAND_UNDERWATER ? LARA_RADIUS_WATER : LARA_RADIUS;
int maxHeight = stand == STAND_UNDERWATER ? LARA_HEIGHT_WATER : LARA_HEIGHT;
int minHeight = 0;
int maxAscent = 256 + 128;
int maxDescent = 0xFFFFFF;
int room = getRoomIndex();
if (state == STATE_WALK || state == STATE_BACK)
maxDescent = maxAscent;
if (state == STATE_STEP_LEFT || state == STATE_STEP_RIGHT)
maxAscent = maxDescent = 64;
if (stand == STAND_ONWATER) {
maxAscent = -1;
maxHeight = 0;
offset.y = -1;
}
bool standHang = stand == STAND_HANG && state != STATE_HANG_UP && state != STATE_HANDSTAND;
if (standHang) {
maxHeight = 0;
maxAscent = maxDescent = 64;
offset = getDir() * (LARA_RADIUS + 32.0f);
offset.y -= LARA_HANG_OFFSET + 32;
}
if (stand == STAND_UNDERWATER) {
offset.y += LARA_HEIGHT_WATER * 0.5f;
}
collision = Collision(this, room, pos, offset, vel, radius, angleExt, minHeight, maxHeight, maxAscent, maxDescent);
if (!standHang && (collision.side == Collision::LEFT || collision.side == Collision::RIGHT)) {
float rot = TURN_WALL_Y * Core::deltaTime;
rotateY((collision.side == Collision::LEFT) ? rot : -rot);
}
if (standHang && collision.side != Collision::FRONT) {
offset.x = offset.z = 0.0f;
minHeight = LARA_HANG_OFFSET;
maxDescent = 0xFFFFFF;
maxAscent = -LARA_HANG_OFFSET;
vec3 p = pos;
collision = Collision(this, room, p, offset, vec3(0.0f), radius, angleExt, minHeight, maxHeight, maxAscent, maxDescent);
if (collision.side == Collision::FRONT)
pos = opos;
}
// get current leading foot in animation
int rightStart = 0;
if (state == STATE_RUN) rightStart = 6;
if (state == STATE_WALK) rightStart = 13;
if (state == STATE_BACK) rightStart = 28;
bool isLeftFoot = animation.frameIndex < rightStart || animation.frameIndex > (rightStart + animation.framesCount / 2);
if (stand == STAND_UNDERWATER) {
if (collision.side == Collision::TOP)
rotateX(-TURN_WALL_X * Core::deltaTime);
if (collision.side == Collision::BOTTOM)
rotateX( TURN_WALL_X * Core::deltaTime);
}
if (stand == STAND_AIR && collision.side == Collision::TOP && velocity.y < 0.0f)
velocity.y = 30.0f;
if (collision.side == Collision::FRONT) {
float floor = collision.info[Collision::FRONT].floor;
/*
switch (angleQuadrant(angleExt - angle.y)) {
case 0 : collision.side = Collision::FRONT; LOG("FRONT\n"); break;
case 1 : collision.side = Collision::RIGHT; LOG("RIGHT\n"); break;
case 2 : collision.side = Collision::BACK; LOG("BACK\n"); break;
case 3 : collision.side = Collision::LEFT; LOG("LEFT\n"); break;
}
*/
if (velocity.dot(getDir()) <= EPS)
collision.side = Collision::NONE;
// hit the wall
switch (stand) {
case STAND_AIR :
if (state == STATE_UP_JUMP || state == STATE_REACH || state == STATE_FALL_BACK)
velocity.x = velocity.z = 0.0f;
if (velocity.x != 0.0f || velocity.z != 0.0f) {
animation.setAnim(ANIM_SMASH_JUMP);
velocity.x = -velocity.x * 0.25f;
velocity.z = -velocity.z * 0.25f;
if (velocity.y < 0.0f)
velocity.y = 30.0f;
}
break;
case STAND_GROUND :
case STAND_HANG :
if (opos.y - floor > (256 * 3 - 128) && state == STATE_RUN)
animation.setAnim(isLeftFoot ? ANIM_SMASH_RUN_LEFT : ANIM_SMASH_RUN_RIGHT);
else if (stand == STAND_HANG)
animation.setAnim(ANIM_HANG, -21);
else if (state != STATE_ROLL_1 && state != STATE_ROLL_2)
animation.setAnim((state == STATE_RUN || state == STATE_WALK) ? (isLeftFoot ? ANIM_STAND_LEFT : ANIM_STAND_RIGHT) : ANIM_STAND);
velocity.x = velocity.z = 0.0f;
break;
case STAND_UNDERWATER :
if (fabsf(angle.x) > TURN_WALL_X_CLAMP)
rotateX(TURN_WALL_X * Core::deltaTime * sign(angle.x));
else
pos.y = opos.y;
break;
default : ;// no smash animation
}
} else {
if (stand == STAND_GROUND) {
float floor = collision.info[Collision::NONE].floor;
float h = floor - opos.y;
if (h >= 128 && (state == STATE_WALK || state == STATE_BACK)) { // descend
if (state == STATE_WALK) animation.setAnim(isLeftFoot ? ANIM_WALK_DESCEND_LEFT : ANIM_WALK_DESCEND_RIGHT);
if (state == STATE_BACK) animation.setAnim(isLeftFoot ? ANIM_BACK_DESCEND_LEFT : ANIM_BACK_DESCEND_RIGHT);
pos.y = float(floor);
} else if (h > -1.0f) {
pos.y = min(float(floor), pos.y += DESCENT_SPEED * Core::deltaTime);
} else if (h > -128) {
pos.y = float(floor);
} else if (h >= -(256 + 128) && (state == STATE_RUN || state == STATE_WALK)) { // ascend
if (state == STATE_RUN) animation.setAnim(isLeftFoot ? ANIM_RUN_ASCEND_LEFT : ANIM_RUN_ASCEND_RIGHT);
if (state == STATE_WALK) animation.setAnim(isLeftFoot ? ANIM_WALK_ASCEND_LEFT : ANIM_WALK_ASCEND_RIGHT);
pos.y = float(floor);
} else
pos.y = float(floor);
}
collision.side = Collision::NONE;
}
if (dozy) stand = STAND_GROUND;
checkRoom();
if (dozy) stand = STAND_UNDERWATER;
}
virtual void applyFlow(TR::Camera &sink) {
if (stand != STAND_UNDERWATER && stand != STAND_ONWATER) return;
vec3 target = vec3(float(sink.x), float(sink.y), float(sink.z));
#ifdef _DEBUG
//delete[] dbgBoxes;
//dbgBoxes = NULL;
#endif
if (box != sink.flags.boxIndex) {
uint16 *boxes;
uint16 count = game->findPath(0xFFFFFF, -0xFFFFFF, false, box, sink.flags.boxIndex, getZones(), &boxes);
if (count > 1) {
#ifdef _DEBUG
//dbgBoxesCount = count;
//dbgBoxes = new uint16[dbgBoxesCount];
//memcpy(dbgBoxes, boxes, sizeof(uint16) * dbgBoxesCount);
#endif
TR::Box &b = level->boxes[boxes[1]];
target.x = (b.minX + b.maxX) * 0.5f;
if (target.y > b.floor)
target.y = float(b.floor);
target.z = (b.minZ + b.maxZ) * 0.5f;
}
}
flowVelocity = vec3(0);
if (!dozy) {
float speed = sink.speed * 6.0f;
flowVelocity.x = clamp(target.x - pos.x, -speed, +speed);
flowVelocity.y = clamp(target.y - pos.y, -speed, +speed);
flowVelocity.z = clamp(target.z - pos.z, -speed, +speed);
if (stand == STAND_ONWATER)
goUnderwater();
}
}
uint32 getMidasMask() {
if (state == STATE_MIDAS_USE)
return BODY_ARM_L3 | BODY_ARM_R3;
uint32 mask = 0;
int frame = animation.frameIndex;
if (frame > 4 ) mask |= BODY_LEG_L3 | BODY_LEG_R3;
if (frame > 69 ) mask |= BODY_LEG_L2;
if (frame > 79 ) mask |= BODY_LEG_L1;
if (frame > 99 ) mask |= BODY_LEG_R2;
if (frame > 119) mask |= BODY_LEG_R1 | BODY_HIP;
if (frame > 134) mask |= BODY_CHEST;
if (frame > 149) mask |= BODY_ARM_L1;
if (frame > 162) mask |= BODY_ARM_L2;
if (frame > 173) mask |= BODY_ARM_L3;
if (frame > 185) mask |= BODY_ARM_R1;
if (frame > 194) mask |= BODY_ARM_R2;
if (frame > 217) mask |= BODY_ARM_R3;
if (frame > 224) mask |= BODY_HEAD;
return mask;
}
virtual void render(Frustum *frustum, MeshBuilder *mesh, Shader::Type type, bool caustics) {
uint32 visMask = visibleMask;
if (Core::pass != Core::passShadow && camera->firstPerson && camera->viewIndex == -1 && game->getCamera() == camera) // hide head in first person view // TODO: fix for firstPerson with viewIndex always == -1
visibleMask &= ~BODY_HEAD;
Controller::render(frustum, mesh, type, caustics);
visibleMask = visMask;
if (braid)
braid->render(mesh);
if (state == STATE_MIDAS_DEATH /* && Core::pass == Core::passCompose */) {
game->setRoomParams(getRoomIndex(), Shader::MIRROR, 1.2f, 1.0f, 0.2f, 1.0f, false);
/* catsuit test
game->setRoomParams(getRoomIndex(), Shader::MIRROR, 0.3f, 0.3f, 0.3f, 1.0f, false);
Core::active.shader->setParam(uLightColor, Core::lightColor[0], MAX_LIGHTS);
Core::active.shader->setParam(uLightPos, Core::lightPos[0], MAX_LIGHTS);
*/
environment->bind(sEnvironment);
Core::setBlendMode(bmAlpha);
visibleMask ^= 0xFFFFFFFF;
Controller::render(frustum, mesh, type, caustics);
visibleMask ^= 0xFFFFFFFF;
Core::setBlendMode(bmNone);
}
}
};
#endif