656 lines
18 KiB
C++
656 lines
18 KiB
C++
/*
|
|
* Copyright (C) 2002,2003,2004,2006 Daniel Heck
|
|
*
|
|
* 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 2
|
|
* 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, write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
*
|
|
*/
|
|
#include "player.hh"
|
|
#include "Inventory.hh"
|
|
#include "display.hh"
|
|
#include "errors.hh"
|
|
#include "sound.hh"
|
|
#include "client.hh"
|
|
#include "server.hh"
|
|
#include "world.hh"
|
|
#include "main.hh"
|
|
|
|
#include "ecl_util.hh"
|
|
|
|
#include <algorithm>
|
|
#include <iostream>
|
|
#include <cassert>
|
|
|
|
using namespace std;
|
|
using namespace enigma;
|
|
using namespace world;
|
|
using world::Actor;
|
|
using enigma::Inventory;
|
|
|
|
namespace
|
|
{
|
|
class PlayerInfo {
|
|
public:
|
|
PlayerInfo();
|
|
|
|
string name;
|
|
Inventory inventory;
|
|
vector<Actor*> actors;
|
|
bool out_of_lives;
|
|
double dead_dtime; // number of seconds the player is already dead
|
|
bool inhibit_pickup;
|
|
};
|
|
|
|
typedef vector<PlayerInfo> PlayerList;
|
|
|
|
struct RespawnInfo {
|
|
RespawnInfo (Actor *a, double t)
|
|
: actor(a), time_left(t)
|
|
{}
|
|
|
|
Actor *actor; // The actor to respawn
|
|
double time_left; // Time left before respawning
|
|
};
|
|
|
|
|
|
struct LevelLocalData {
|
|
// Functions
|
|
LevelLocalData() {}
|
|
|
|
void respawn_dead_actors(double dtime);
|
|
void resurrect_actor (Actor *a);
|
|
bool remove_extralife (Actor *a);
|
|
|
|
// Variables
|
|
vector<RespawnInfo> respawn_list;
|
|
};
|
|
}
|
|
|
|
/* -------------------- PlayerInfo -------------------- */
|
|
|
|
PlayerInfo::PlayerInfo ()
|
|
: name(),
|
|
inventory(),
|
|
actors(),
|
|
out_of_lives(false),
|
|
dead_dtime(0),
|
|
inhibit_pickup(false)
|
|
{}
|
|
|
|
/* -------------------- LevelLocalData -------------------- */
|
|
|
|
|
|
void LevelLocalData::respawn_dead_actors(double dtime)
|
|
{
|
|
for (unsigned i=0; i<respawn_list.size(); ) {
|
|
RespawnInfo &info = respawn_list[i];
|
|
|
|
info.time_left -= dtime;
|
|
if (info.time_left < 0) {
|
|
// if (lua::CallFunc (lua::LevelState(), "Respawn", Value()) != 0) {
|
|
// throw enigma_levels::XLevelRuntime(string("Calling 'Respawn' failed:\n")+lua::LastError(lua::LevelState()));
|
|
|
|
info.actor->respawn();
|
|
respawn_list.erase(respawn_list.begin()+i);
|
|
continue; // don't increment i
|
|
}
|
|
++i;
|
|
}
|
|
}
|
|
|
|
void LevelLocalData::resurrect_actor (Actor *a)
|
|
{
|
|
const double RESPAWN_TIME = 1.5;
|
|
|
|
// a->find_respawnpos();
|
|
world::SendMessage(a, "resurrect");
|
|
remove_extralife(a);
|
|
respawn_list.push_back(RespawnInfo(a, RESPAWN_TIME));
|
|
}
|
|
|
|
bool LevelLocalData::remove_extralife (Actor *a)
|
|
{
|
|
Inventory *inv = player::GetInventory(a);
|
|
int idx = inv->find("it-extralife");
|
|
|
|
if (idx == -1) // no extralife found
|
|
return false;
|
|
else {
|
|
delete inv->yield_item(idx);
|
|
player::RedrawInventory (inv);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
|
|
/* -------------------- Local variables -------------------- */
|
|
|
|
namespace
|
|
{
|
|
LevelLocalData leveldat;
|
|
PlayerList players(2); // this currently has always size 2
|
|
unsigned icurrent_player = 0;
|
|
std::vector<Actor *> unassignedActors;
|
|
}
|
|
|
|
|
|
/* -------------------- Functions -------------------- */
|
|
|
|
void player::NewGame (bool isRestart) {
|
|
int nplayers = 2; // Always prepare for two players
|
|
std::vector<int> extralives(2);
|
|
|
|
// calculate number of extralives
|
|
for (int i=0; i<nplayers; ++i) {
|
|
if (isRestart) {
|
|
// count existing number of extralives
|
|
int idxLife = -1;
|
|
extralives[i] = -1;
|
|
do {
|
|
++extralives[i];
|
|
idxLife = players[i].inventory.find("it-extralife", ++idxLife);
|
|
} while (idxLife != -1);
|
|
} else {
|
|
// new game provides 2 extralives
|
|
extralives[i] = 2;
|
|
}
|
|
}
|
|
|
|
players.clear();
|
|
players.resize(nplayers);
|
|
|
|
for (int i=0; i<nplayers; ++i) {
|
|
Inventory *inv = GetInventory(i);
|
|
|
|
for (int j = 0 ; j < extralives[i]; j++)
|
|
inv->add_item (MakeItem (world::it_extralife));
|
|
}
|
|
|
|
unassignedActors.clear();
|
|
}
|
|
|
|
void player::AddYinYang ()
|
|
{
|
|
for (unsigned i=0; i<players.size(); ++i) {
|
|
Inventory *inv = GetInventory (i);
|
|
if (inv->find ("it-yinyang") == -1)
|
|
inv->add_item (world::MakeItem (world::it_yinyang));
|
|
}
|
|
}
|
|
|
|
void player::LevelLoaded(bool isRestart)
|
|
{
|
|
if (server::TwoPlayerGame && server::SingleComputerGame)
|
|
AddYinYang();
|
|
RedrawInventory ();
|
|
}
|
|
|
|
void player::PrepareLevel()
|
|
{
|
|
// Clear up the inventories of all players: keep only extra lifes.
|
|
for (unsigned iplayer=0; iplayer<players.size(); ++iplayer)
|
|
{
|
|
Inventory *inv = GetInventory(iplayer);
|
|
int nextralifes=0;
|
|
for (size_t i=0; i<inv->size(); ++i)
|
|
if (get_id (inv->get_item(i)) == world::it_extralife)
|
|
nextralifes += 1;
|
|
inv->clear();
|
|
for (int i=0; i<nextralifes; ++i)
|
|
inv->add_item (world::MakeItem (world::it_extralife));
|
|
|
|
players[iplayer].actors.clear();
|
|
}
|
|
|
|
SetCurrentPlayer(0);
|
|
leveldat = LevelLocalData();
|
|
}
|
|
|
|
void player::LevelFinished()
|
|
{
|
|
for (unsigned i=0; i<players.size(); ++i) {
|
|
for (unsigned j=0; j<players[i].actors.size(); ++j) {
|
|
Actor *a = players[i].actors[j];
|
|
world::SendMessage(a, "disappear");
|
|
world::KillRubberBands (a);
|
|
}
|
|
}
|
|
}
|
|
|
|
Inventory * player::GetInventory (int iplayer)
|
|
{
|
|
return &players[iplayer].inventory;
|
|
}
|
|
|
|
|
|
Inventory * player::GetInventory (Actor *a)
|
|
{
|
|
if (const Value *v = a->get_attrib("player"))
|
|
return GetInventory(to_int(*v));
|
|
return 0;
|
|
}
|
|
|
|
|
|
bool player::WieldedItemIs (Actor *a, const string &kind)
|
|
{
|
|
if (Inventory *inv = GetInventory(a))
|
|
if (Item *it = inv->get_item(0))
|
|
return it->is_kind(kind);
|
|
return false;
|
|
}
|
|
|
|
|
|
int player::CurrentPlayer() {
|
|
return icurrent_player;
|
|
}
|
|
|
|
void player::SetCurrentPlayer(unsigned iplayer)
|
|
{
|
|
if (iplayer >= players.size())
|
|
Log << ecl::strf("SetCurrentPlayer: no such player %d\n", iplayer);
|
|
else {
|
|
icurrent_player = iplayer;
|
|
RedrawInventory (GetInventory(iplayer));
|
|
}
|
|
}
|
|
|
|
unsigned player::NumberOfRealPlayers() {
|
|
unsigned real_players = 0;
|
|
|
|
for (unsigned i=0; i<players.size(); ++i) {
|
|
if (!players[i].actors.empty()) {
|
|
++real_players;
|
|
}
|
|
}
|
|
|
|
return real_players;
|
|
}
|
|
|
|
/*! Sets respawn positions for black or white actors. */
|
|
void player::SetRespawnPositions(GridPos pos, bool black)
|
|
{
|
|
ecl::V2 center = pos.center();
|
|
|
|
for (unsigned i=0; i<players.size(); ++i) {
|
|
vector<Actor *> &al = players[i].actors;
|
|
|
|
for (unsigned j=0; j<al.size(); ++j) {
|
|
const Value *val = al[j]->get_attrib(black ? "blackball" : "whiteball");
|
|
if (val) {
|
|
al[j]->set_respawnpos(center);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*! Remove respawn positions for black or white actors */
|
|
void player::RemoveRespawnPositions(bool black) {
|
|
for (unsigned i=0; i<players.size(); ++i) {
|
|
vector<Actor *> &al = players[i].actors;
|
|
|
|
for (unsigned j=0; j<al.size(); ++j) {
|
|
const Value *val = al[j]->get_attrib(black ? "blackball" : "whiteball");
|
|
if (val) {
|
|
al[j]->remove_respawnpos();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void player::Suicide()
|
|
{
|
|
for (unsigned i=0; i<players.size(); ++i) {
|
|
vector<Actor *> &al = players[i].actors;
|
|
for (unsigned j=0; j<al.size(); ++j) {
|
|
world::SendMessage(al[j], "suicide");
|
|
}
|
|
}
|
|
}
|
|
|
|
Actor *player::ReplaceActor (unsigned iplayer, Actor *old, Actor *a)
|
|
{
|
|
if (iplayer >= players.size())
|
|
server::RaiseError ("Invalid actor number");
|
|
|
|
vector<Actor *> &al = players[iplayer].actors;
|
|
for (unsigned i=0; i<al.size(); ++i) {
|
|
if (al[i] == old) {
|
|
al[i] = a;
|
|
return old;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
void player::AddActor (unsigned iplayer, Actor *a)
|
|
{
|
|
if (iplayer >= players.size())
|
|
server::RaiseError ("Invalid actor number");
|
|
|
|
world::ReleaseActor(a);
|
|
players[iplayer].actors.push_back(a);
|
|
|
|
if (players[iplayer].actors.size() == 1) {
|
|
// the ``main actor'' was set
|
|
client::Msg_PlayerPosition (iplayer, a->get_pos());
|
|
}
|
|
}
|
|
|
|
bool player::HasActor(unsigned iplayer, Actor *a) {
|
|
if (iplayer >= 0 && iplayer < players.size()) {
|
|
for (int i = 0; i < players[iplayer].actors.size(); i++) {
|
|
if (players[iplayer].actors[i] == a)
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void player::SwapPlayers()
|
|
{
|
|
if (NumberOfRealPlayers() >= 2) {
|
|
SetCurrentPlayer(1-icurrent_player);
|
|
}
|
|
}
|
|
|
|
static bool has_extralive(unsigned pl, unsigned num) {
|
|
size_t idx = 0;
|
|
for (int i = 0; i < num; i++) {
|
|
int idxLife = players[pl].inventory.find("it-extralife", idx);
|
|
if (idxLife == -1)
|
|
return false;
|
|
else
|
|
idx = idxLife + 1;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool resurrect_actor (unsigned pl, Actor *a)
|
|
{
|
|
assert(server::ConserveLevel); // no resurrection otherwise!
|
|
|
|
bool has_life = has_extralive(pl, 1);
|
|
if (has_life)
|
|
leveldat.resurrect_actor(a); // = resurrect with delay
|
|
else
|
|
players[pl].out_of_lives = true;
|
|
return has_life;
|
|
}
|
|
|
|
void player::AddUnassignedActor (Actor *a) {
|
|
unassignedActors.push_back(a);
|
|
}
|
|
|
|
static void CheckDeadActors()
|
|
{
|
|
bool toggle_player = false;
|
|
const unsigned NO_PLAYER = UINT_MAX;
|
|
unsigned toggle_to_player = NO_PLAYER;
|
|
bool new_game = false; // complete restart (new lives)
|
|
|
|
// to live means to be not dead and to be able to move
|
|
for (int pl = -1; pl<(int)players.size(); ++pl) { // -1 are unassigned actors
|
|
vector<Actor*>& actors = (pl == -1) ? unassignedActors : players[pl].actors;
|
|
bool has_living_actor = false; // controllable and living
|
|
std::map<std::string, int> essMap;
|
|
std::map<std::string, int>::iterator itEss;
|
|
|
|
for (size_t i=0; i<actors.size(); ++i) {
|
|
Actor *a = actors[i];
|
|
std::string essId;
|
|
if (!a->string_attrib ("essential_id", &essId))
|
|
essId = a->get_traits().name;
|
|
int essential = a->int_attrib("essential");
|
|
// count number of necessary actors per kind
|
|
if (essential == 1)
|
|
--essMap[essId];
|
|
|
|
if (!a->is_dead() ||
|
|
(pl >= 0 && server::ConserveLevel && resurrect_actor(pl, a))) {
|
|
// actor is still alive
|
|
if (pl >= 0 && a->controlled_by(pl) && a->get_mouseforce() != 0.0) {
|
|
has_living_actor = true;
|
|
}
|
|
// count number of alive actors per kind
|
|
if (essential >= 0)
|
|
++essMap[essId];
|
|
}
|
|
else {
|
|
// player is dead and could not resurrect
|
|
if (essential == -1) {
|
|
// actor is personnally essential but dead
|
|
new_game = true;
|
|
}
|
|
}
|
|
}
|
|
// check if for any kind we have less living actors as required
|
|
for (itEss = essMap.begin(); itEss != essMap.end(); itEss++) {
|
|
if ((*itEss).second < 0)
|
|
new_game = true;
|
|
}
|
|
|
|
if (has_living_actor) {
|
|
if (toggle_to_player == NO_PLAYER)
|
|
toggle_to_player = pl;
|
|
}
|
|
else {
|
|
if (pl == icurrent_player)
|
|
// check if player has yinyang for single gamer mode
|
|
if (player::GetInventory(pl)->find("it-yinyang",0) >= 0)
|
|
toggle_player = true;
|
|
else
|
|
new_game = true;
|
|
}
|
|
}
|
|
|
|
// if no_living_player -> toggle_player is true and toggle_to_player is NO_PLAYER
|
|
// => new_game is set to true below
|
|
|
|
if ((server::ConserveLevel == false) && !server::IsRestartingLevel() &&
|
|
(new_game || (toggle_player && toggle_to_player == NO_PLAYER))) {
|
|
// check if we have enough extralives for a restart instead of new game
|
|
std::vector<int> numDead;
|
|
bool reset_level = true;
|
|
for (unsigned pl = 0; pl<players.size(); ++pl) {
|
|
numDead.push_back(0);
|
|
vector<Actor*>& actors = players[pl].actors;
|
|
for (size_t i = 0; i<actors.size(); ++i) {
|
|
Actor *a = actors[i];
|
|
if (a->is_dead()) {
|
|
numDead[pl]++;
|
|
}
|
|
}
|
|
if (!has_extralive(pl, numDead[pl])) {
|
|
reset_level = false;
|
|
break;
|
|
}
|
|
}
|
|
if (reset_level) {
|
|
for (unsigned pl = 0; pl<players.size(); ++pl) {
|
|
Inventory *inv = player::GetInventory(pl);
|
|
for (int i=0; i<numDead[pl]; i++) {
|
|
int idx = inv->find("it-extralife");
|
|
ASSERT (idx != -1, XLevelRuntime, "Missing extralife for restart of level");
|
|
delete inv->yield_item(idx);
|
|
}
|
|
}
|
|
server::RestartLevel(); // should restart w/o scrolling
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (new_game) {
|
|
server::Msg_RestartGame();
|
|
}
|
|
else if (toggle_player) {
|
|
if (toggle_to_player == NO_PLAYER)
|
|
server::Msg_RestartGame();
|
|
else
|
|
player::SetCurrentPlayer(toggle_to_player);
|
|
}
|
|
}
|
|
|
|
Actor *player::GetMainActor (unsigned iplayer)
|
|
{
|
|
vector<Actor *> &actors = players[iplayer].actors;
|
|
return actors.empty() ? 0 : actors[0];
|
|
}
|
|
|
|
void player::Tick(double dtime)
|
|
{
|
|
STATUSBAR->set_counter (server::GetMoveCounter());
|
|
|
|
|
|
// Tell clients about position of main actor for stereo sound and
|
|
// screen position
|
|
for (unsigned iplayer = 0; iplayer < players.size(); ++iplayer) {
|
|
if (Actor *ac = GetMainActor(iplayer))
|
|
client::Msg_PlayerPosition (iplayer, ac->get_pos());
|
|
}
|
|
|
|
// Respawn actors that have been dead for a certain amount of time
|
|
leveldat.respawn_dead_actors(dtime);
|
|
|
|
// Update the respawn list or restart the game when all actors are
|
|
// dead and no extra lifes are left.
|
|
CheckDeadActors();
|
|
}
|
|
|
|
void player::InhibitPickup(bool flag) {
|
|
players[icurrent_player].inhibit_pickup = flag;
|
|
}
|
|
|
|
/*! Return pointer to inventory if actor may pick up items, 0
|
|
otherwise. */
|
|
Inventory *player::MayPickup(Actor *a, Item *it)
|
|
{
|
|
int iplayer=-1;
|
|
a->int_attrib("player", &iplayer);
|
|
if (iplayer < 0 || (unsigned)iplayer >= players.size()) {
|
|
// cerr << "PickupItem: illegal 'player' entry\n";
|
|
return 0;
|
|
}
|
|
|
|
Inventory *inv = GetInventory(iplayer);
|
|
bool dont_pickup = players[iplayer].inhibit_pickup
|
|
|| a->is_flying()
|
|
|| !inv->willAddItem(it)
|
|
|| a->is_dead();
|
|
|
|
return dont_pickup ? 0 : inv;
|
|
}
|
|
|
|
void player::PickupItem (Actor *a, GridPos p)
|
|
{
|
|
if (Inventory *inv = MayPickup(a, GetField(p)->item)) {
|
|
if (Item *item = world::YieldItem(p)) {
|
|
item->on_pickup(a);
|
|
inv->add_item(item);
|
|
RedrawInventory (inv);
|
|
sound::EmitSoundEvent ("pickup", p.center());
|
|
}
|
|
}
|
|
}
|
|
|
|
void player::PickupStoneAsItem (Actor *a, enigma::GridPos p)
|
|
{
|
|
if (Inventory *inv = MayPickup(a, GetField(p)->item))
|
|
{
|
|
if (world::Stone *stone = world::YieldStone(p))
|
|
{
|
|
string kind = stone->get_kind();
|
|
if (kind[0] == 's')
|
|
kind[0] = 'i';
|
|
|
|
if (Item *item = world::MakeItem(kind.c_str())) {
|
|
KillRubberBands(stone);
|
|
world::DisposeObject (stone);
|
|
inv->add_item(item);
|
|
player::RedrawInventory(inv);
|
|
sound::EmitSoundEvent ("pickup", p.center());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void player::ActivateFirstItem()
|
|
{
|
|
Inventory &inv = players[icurrent_player].inventory;
|
|
|
|
|
|
if (inv.size() > 0) {
|
|
Item *it = inv.get_item (0);
|
|
world::Actor *ac = 0;
|
|
GridPos p;
|
|
bool can_drop_item = false;
|
|
if (!players[icurrent_player].actors.empty()) {
|
|
ac = players[icurrent_player].actors[0];
|
|
p = GridPos(ac->get_pos());
|
|
can_drop_item = ac->can_drop_items();
|
|
}
|
|
|
|
switch (it->activate(ac, p)) {
|
|
case world::ITEM_DROP:
|
|
// only drop if no item underneath and actor allows it
|
|
if (it->can_drop_at(p) && can_drop_item) {
|
|
it = inv.yield_first ();
|
|
RedrawInventory (&inv);
|
|
it->drop(ac, p);
|
|
}
|
|
break;
|
|
case world::ITEM_KILL:
|
|
DisposeObject (inv.yield_first ());
|
|
RedrawInventory (&inv);
|
|
break;
|
|
case world::ITEM_KEEP:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void player::RotateInventory(int dir)
|
|
{
|
|
sound::EmitSoundEvent ("invrotate", ecl::V2());
|
|
Inventory &inv = players[icurrent_player].inventory;
|
|
if (dir == 1)
|
|
inv.rotate_left ();
|
|
else
|
|
inv.rotate_right ();
|
|
RedrawInventory (&inv);
|
|
}
|
|
|
|
|
|
/** Update the specified inventory on the screen, provided it is the
|
|
inventory of the current player. For all other inventories, this
|
|
function does nothing. */
|
|
void player::RedrawInventory (Inventory *inv)
|
|
{
|
|
if (inv == GetInventory (CurrentPlayer()))
|
|
RedrawInventory();
|
|
}
|
|
|
|
|
|
void player::RedrawInventory()
|
|
{
|
|
Inventory *inv = GetInventory (CurrentPlayer());
|
|
std::vector<std::string> modelnames;
|
|
for (size_t i=0; i<inv->size(); ++i) {
|
|
world::Item *it = inv->get_item(i);
|
|
modelnames.push_back(it->get_inventory_model());
|
|
}
|
|
STATUSBAR->set_inventory (modelnames);
|
|
}
|
|
|