/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
#include "ultima/ultima8/world/gravity_process.h"
#include "ultima/ultima8/audio/audio_process.h"
#include "ultima/ultima8/kernel/kernel.h"
#include "ultima/ultima8/ultima8.h"
#include "ultima/ultima8/world/actors/actor.h"
#include "ultima/ultima8/world/actors/actor_anim_process.h"
#include "ultima/ultima8/world/get_object.h"
#include "ultima/ultima8/world/current_map.h"
#include "ultima/ultima8/world/world.h"
namespace Ultima {
namespace Ultima8 {
DEFINE_RUNTIME_CLASSTYPE_CODE(GravityProcess)
GravityProcess::GravityProcess()
: Process(), _xSpeed(0), _ySpeed(0), _zSpeed(0), _gravity(0) {
}
GravityProcess::GravityProcess(Item *item, int gravity)
: _xSpeed(0), _ySpeed(0), _zSpeed(0), _gravity(gravity) {
assert(item);
_itemNum = item->getObjId();
_type = 0x203; // CONSTANT!
}
void GravityProcess::init() {
Item *item = getItem(_itemNum);
assert(item);
item->setGravityPID(getPid());
Actor *actor = dynamic_cast(item);
if (actor) {
actor->setFallStart(actor->getZ());
}
}
void GravityProcess::move(int xs, int ys, int zs) {
_xSpeed += xs;
_ySpeed += ys;
_zSpeed += zs;
}
void GravityProcess::setGravity(int gravity) {
// only apply _gravity if stronger than current _gravity
//!!! is this correct?
if (gravity > _gravity)
_gravity = gravity;
}
void GravityProcess::run() {
// move item in (xs,ys,zs) direction
Item *item = getItem(_itemNum);
if (!item) {
terminate();
return;
}
// There should never be more than one gravity on an object
assert(item->getGravityPID() == _pid);
Actor *actor = dynamic_cast(item);
if (actor && actor->getFallStart() < actor->getZ()) {
actor->setFallStart(actor->getZ());
}
// What to do:
// - check if item can move to the given position
// (in intervals in the z direction, since the z movement
// can be quite large)
// - if item can move, move it
// - if item can't move, it hit something:
// - bounce off the item (need to consider FLG_LOW_FRICTION?)
// - call the relevant events: hit/gothit ?
Point3 pt = item->getLocation();
// Shouldn't go negative as original did not allow it
if (pt.z <= 0 && _zSpeed < 0) {
terminateDeferred();
fallStopped();
return;
}
int32 tx, ty, tz;
tx = pt.x + _xSpeed;
ty = pt.y + _ySpeed;
tz = pt.z + _zSpeed;
if (tz < 0)
tz = 0;
//#define BOUNCE_DIAG
ObjId hititemid;
uint8 dirs;
int32 dist = item->collideMove(tx, ty, tz, false, false, &hititemid, &dirs);
if (dist == 0x4000 && !hititemid) {
// didn't hit anything.
_zSpeed -= _gravity;
return;
}
// TODO: after doing a partial move we may still want to do another
// partial move in this ::run() call to move the full speed this frame.
// The effect might not be visible enough to be worth the trouble though.
// Item was blocked
// We behave differently depending on which direction was blocked.
// We only consider stopping to bounce when blocked purely in the
// downward-Z direction. Other directions always bounce, reducing speed in
// the blocked directions
// only blocked going down?
if (dirs == 4 && _zSpeed < 0) {
// If it landed on top of hititem and hititem is not land, the item
// should always bounce.
bool termFlag = true;
Item *hititem = getItem(hititemid);
if (!hititem)
return; // shouldn't happen..
CurrentMap *cm = World::get_instance()->getCurrentMap();
Box target = item->getWorldBox();
Box empty;
PositionInfo info = cm->getPositionInfo(target, empty, item->getShapeInfo()->_flags, _itemNum);
if (!info.valid || !info.supported) {
// Reset speed and continue to slip off
termFlag = false;
_zSpeed = 0;
pt = item->getCentre();
target = hititem->getWorldBox();
if (ABS(_xSpeed) < 16) {
if (pt.x + 16 > target._x)
_xSpeed = 16;
else if (pt.x - 16 < target._x - target._xd)
_xSpeed = -16;
}
if (ABS(_ySpeed) < 16) {
if (pt.y + 16 > target._y)
_ySpeed = 16;
else if (pt.y - 16 < target._y - target._yd)
_ySpeed = -16;
}
} else if (_zSpeed < -2 && !actor) {
#ifdef BOUNCE_DIAG
debugC(kDebugObject, "item %u bounce [%u]: hit %u",
_itemNum, Kernel::get_instance()->getFrameNum(), hititem->getObjId());
#endif
if (GAME_IS_U8 && (!hititem->getShapeInfo()->is_land() || _zSpeed < -2 * _gravity)) {
// Bounce!
termFlag = false;
#ifdef BOUNCE_DIAG
int xspeedold = _xSpeed;
int yspeedold = _ySpeed;
int zspeedold = _zSpeed;
#endif
_zSpeed = 0 - _zSpeed / 3;
int approx_v = ABS(_xSpeed) + ABS(_ySpeed) + _zSpeed;
Common::RandomSource &rs = Ultima8Engine::get_instance()->getRandomSource();
// Apply an impulse on the x/y plane in a random direction
// in a 180 degree pie around the original vector in x/y
double heading_r = atan2((double)_ySpeed, (double)_xSpeed);
double deltah_r = static_cast(rs.getRandomNumber(UINT_MAX)) * M_PI / UINT_MAX - M_PI / 2.0;
#ifdef BOUNCE_DIAG
double headingold_r = heading_r;
#endif
heading_r += deltah_r;
if (heading_r > M_PI) heading_r -= 2 * M_PI;
if (heading_r < -M_PI) heading_r += 2 * M_PI;
_ySpeed += static_cast(sin(heading_r) *
static_cast(approx_v));
_xSpeed += static_cast(cos(heading_r) *
static_cast(approx_v));
if (hititem->getShapeInfo()->is_land()) {
// Bouncing off land; this bounce approximates what's
// seen in the original U8 when the key thrown by
// Kilandra's daughters ghost lands on the grass.
_xSpeed /= 4;
_ySpeed /= 4;
_zSpeed /= 2;
if (_zSpeed == 0)
termFlag = true;
} else {
// Not on land; this bounce approximates what's seen
// in the original U8 when Kilandra's daughters ghost
// throws a key at the Avatar's head
if (ABS(_ySpeed) > 2) _ySpeed /= 2;
if (ABS(_xSpeed) > 2) _xSpeed /= 2;
}
#ifdef BOUNCE_DIAG
debugc(kDebugObject, "item %u bounce [%u]: speed was (%d, %d, %d) new _zSpeed %d heading %lf impulse %lf (%d, %d), termFlag: %d",
_itemNum, Kernel::get_instance()->getFrameNum(),
xspeedold, yspeedold, zspeedold
_zSpeed, headingold_r, heading_r,
(_xSpeed - xspeedold), (_ySpeed - yspeedold), termFlag);
#endif
} else {
#ifdef BOUNCE_DIAG
debugC(kDebugObject, "item %u bounce [%u]: no bounce",
_itemNum, Kernel::get_instance()->getFrameNum());
#endif
}
} else {
#ifdef BOUNCE_DIAG
debugC(kDebugObject, "item %u bounce [%u]: slow hit",
_itemNum, Kernel::get_instance()->getFrameNum());
#endif
}
if (termFlag) {
item->clearFlag(Item::FLG_BOUNCING);
terminateDeferred();
} else {
item->setFlag(Item::FLG_BOUNCING);
}
fallStopped();
} else {
// blocked in some other direction than strictly downward
// invert and decrease speed in all blocked directions
#ifdef BOUNCE_DIAG
int xspeedold = _xSpeed;
int yspeedold = _ySpeed;
int zspeedold = _zSpeed;
#endif
if (dirs & 1)
_xSpeed = -_xSpeed / 2;
if (dirs & 2)
_ySpeed = -_ySpeed / 2;
if (dirs & 4)
_zSpeed = -_zSpeed / 2;
#ifdef BOUNCE_DIAG
debugC(kDebugObject, "item %u bounce [%u]: speed was (%d, %d, %d) new speed (%d, %d, %d)",
_itemNum, Kernel::get_instance()->getFrameNum(),
xspeedold, yspeedold, zspeedold ,
_xSpeed, _ySpeed, _zSpeed);
#endif
item->setFlag(Item::FLG_BOUNCING);
}
}
void GravityProcess::terminate() {
//signal item GravityProcess is gone
Item *item = getItem(_itemNum);
if (item) {
// This is strange, but not impossible (one terminates
// and another starts before terminate() gets called).
// Don't reset the item's gravityPID in this case.
if (item->getGravityPID() != 0 && item->getGravityPID() != _pid)
warning("GravityProcess::terminate %d on item %d which now has gravityPID %d",
_pid, _itemNum, item->getGravityPID());
else
item->setGravityPID(0);
// no longer bouncing
item->clearFlag(Item::FLG_BOUNCING);
}
Process::terminate();
}
void GravityProcess::fallStopped() {
// actors take a hit if they fall
// CHECKME: might need to do a 'die' animation even if actor is dead
Actor *actor = getActor(_itemNum);
if (actor && !actor->isDead()) {
int height = actor->getFallStart() - actor->getZ();
if (GAME_IS_U8)
actorFallStoppedU8(actor, height);
else
actorFallStoppedCru(actor, height);
}
}
void GravityProcess::actorFallStoppedU8(Actor *actor, int height) {
if (height >= 80) {
int damage = 0;
if (height < 104) {
// medium fall: take some damage
damage = (height - 72) / 4;
} else {
// high fall: die
damage = actor->getHP();
}
actor->receiveHit(0, actor->getDir(), damage,
WeaponInfo::DMG_FALLING | WeaponInfo::DMG_PIERCE);
// 'ooof'
AudioProcess *audioproc = AudioProcess::get_instance();
if (audioproc) audioproc->playSFX(51, 250, _itemNum, 0); // CONSTANT!
}
if (!actor->isDead() && actor->getLastAnim() != Animation::die && !actor->hasActorFlags(Actor::ACT_STUNNED)) {
Kernel *kernel = Kernel::get_instance();
// play land animation, overriding other animations
kernel->killProcesses(_itemNum, ActorAnimProcess::ACTOR_ANIM_PROC_TYPE, false); // CONSTANT!
ProcId lpid = actor->doAnim(Animation::land, dir_current);
if (actor->isInCombat()) {
// need to get back to a combat stance to prevent weapon from
// being drawn again
ProcId spid = actor->doAnim(Animation::combatStand, dir_current);
Process *sp = kernel->getProcess(spid);
sp->waitFor(lpid);
}
}
}
void GravityProcess::actorFallStoppedCru(Actor *actor, int height) {
Animation::Sequence lastanim = actor->getLastAnim();
Kernel *kernel = Kernel::get_instance();
if (!actor->isDead() &&
height / 8 > 2 &&
(lastanim != Animation::quickJumpCru &&
lastanim != Animation::combatRollLeft &&
lastanim != Animation::combatRollRight &&
lastanim != Animation::kneelCombatRollLeft &&
lastanim != Animation::kneelCombatRollRight &&
lastanim != Animation::run &&
lastanim != Animation::jumpForward &&
lastanim != Animation::combatRunSmallWeapon &&
lastanim != Animation::combatRunLargeWeapon)) {
// play land animation, overriding other animations
kernel->killProcesses(_itemNum, ActorAnimProcess::ACTOR_ANIM_PROC_TYPE, false); // CONSTANT!
ProcId lpid = actor->doAnim(Animation::jumpLanding, dir_current);
Animation::Sequence nextanim = actor->isInCombat() ? Animation::combatStand : Animation::stand;
actor->doAnimAfter(nextanim, dir_current, lpid);
// 'ooof' (Note: same sound no is used in No Remorse and No Regret)
AudioProcess *audioproc = AudioProcess::get_instance();
if (audioproc) audioproc->playSFX(0x8f, 250, _itemNum, 0); // CONSTANT!
} else {
Process *currentanim = kernel->findProcess(_itemNum, ActorAnimProcess::ACTOR_ANIM_PROC_TYPE);
if (currentanim) {
// TODO: Is this the right thing?
currentanim->wakeUp(0);
}
}
}
Common::String GravityProcess::dumpInfo() const {
return Process::dumpInfo() +
Common::String::format(", _gravity: %d, speed: (%d, %d, %d)", _gravity, _xSpeed, _ySpeed, _zSpeed);
}
void GravityProcess::saveData(Common::WriteStream *ws) {
Process::saveData(ws);
ws->writeUint32LE(static_cast(_gravity));
ws->writeUint32LE(static_cast(_xSpeed));
ws->writeUint32LE(static_cast(_ySpeed));
ws->writeUint32LE(static_cast(_zSpeed));
}
bool GravityProcess::loadData(Common::ReadStream *rs, uint32 version) {
if (!Process::loadData(rs, version)) return false;
_gravity = static_cast(rs->readUint32LE());
_xSpeed = static_cast(rs->readUint32LE());
_ySpeed = static_cast(rs->readUint32LE());
_zSpeed = static_cast(rs->readUint32LE());
return true;
}
} // End of namespace Ultima8
} // End of namespace Ultima