3560 lines
110 KiB
C++
3560 lines
110 KiB
C++
/*
|
||
* Copyright (C) 2002,2003,2004 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 "errors.hh"
|
||
#include "laser.hh"
|
||
#include "sound.hh"
|
||
#include "server.hh"
|
||
#include "player.hh"
|
||
#include "Inventory.hh"
|
||
#include "stones_internal.hh"
|
||
#include "actors.hh"
|
||
|
||
#include "ecl_util.hh"
|
||
|
||
#include <algorithm>
|
||
#include <iostream>
|
||
|
||
using namespace std;
|
||
using namespace world;
|
||
using namespace stones;
|
||
|
||
|
||
/* -------------------- RotatorStone -------------------- */
|
||
namespace
|
||
{
|
||
class RotatorStone : public PhotoStone {
|
||
public:
|
||
RotatorStone(bool clockwise_, bool movable_)
|
||
: PhotoStone("st-rotator"), clockwise(clockwise_), movable(movable_)
|
||
{}
|
||
|
||
private:
|
||
static const double RATE;
|
||
static const double IMPULSE_DELAY;
|
||
|
||
bool clockwise;
|
||
bool movable;
|
||
|
||
Stone *clone() { return new RotatorStone(clockwise, movable); }
|
||
void dispose() { delete this; }
|
||
|
||
// Stone interface
|
||
void on_creation (GridPos p) {
|
||
Stone::on_creation(p);
|
||
photo_activate();
|
||
}
|
||
void on_removal (GridPos p){
|
||
photo_deactivate();
|
||
Stone::on_removal(p);
|
||
}
|
||
|
||
void send_impulses() {
|
||
GridPos p = get_pos();
|
||
|
||
if (clockwise) {
|
||
send_impulse (move(p, NORTH), EAST, IMPULSE_DELAY);
|
||
send_impulse (move(p, EAST), SOUTH, IMPULSE_DELAY);
|
||
send_impulse (move(p, SOUTH), WEST, IMPULSE_DELAY);
|
||
send_impulse (move(p, WEST), NORTH, IMPULSE_DELAY);
|
||
} else {
|
||
send_impulse (move(p, NORTH), WEST, IMPULSE_DELAY);
|
||
send_impulse (move(p, EAST), NORTH, IMPULSE_DELAY);
|
||
send_impulse (move(p, SOUTH), EAST, IMPULSE_DELAY);
|
||
send_impulse (move(p, WEST), SOUTH, IMPULSE_DELAY);
|
||
}
|
||
}
|
||
|
||
|
||
void init_model() {
|
||
set_anim(clockwise ? "st-rotator-right" : "st-rotator-left");
|
||
}
|
||
void animcb() {
|
||
init_model();
|
||
send_impulses();
|
||
}
|
||
|
||
bool is_movable () const { return movable; }
|
||
|
||
void actor_hit (const StoneContact &sc) {
|
||
if (player::WieldedItemIs (sc.actor, "it-wrench")) {
|
||
clockwise = !clockwise;
|
||
init_model();
|
||
}
|
||
|
||
if (movable)
|
||
maybe_push_stone(sc);
|
||
}
|
||
|
||
void on_impulse(const Impulse& impulse) {
|
||
if (movable)
|
||
move_stone(impulse.dir);
|
||
}
|
||
|
||
// PhotoStone interface
|
||
void notify_laseron() {
|
||
clockwise = !clockwise;
|
||
init_model();
|
||
}
|
||
void notify_laseroff() {}
|
||
};
|
||
|
||
const double RotatorStone::RATE = 1.0;
|
||
const double RotatorStone::IMPULSE_DELAY = 0.1;
|
||
}
|
||
|
||
|
||
/* -------------------- PullStone -------------------- */
|
||
|
||
// When pushed this stone acts like pulled.
|
||
// When pushed by an actor it exchanges its position with the actor.
|
||
|
||
namespace
|
||
{
|
||
struct PulledActor {
|
||
// Variables
|
||
Actor *actor;
|
||
V2 speed;
|
||
|
||
// Constructor
|
||
PulledActor(Actor *actor_, const V2& speed_)
|
||
: actor(actor_), speed(speed_)
|
||
{
|
||
}
|
||
|
||
};
|
||
|
||
class PullInfo {
|
||
list<PulledActor> actors;
|
||
YieldedGridStone *ystone;
|
||
|
||
public:
|
||
PullInfo(Stone *st)
|
||
: ystone(st ? new YieldedGridStone(st) : 0)
|
||
{}
|
||
~PullInfo() { delete ystone; }
|
||
|
||
void add_actor(Actor *actor, const V2& speed) {
|
||
actors.push_back(PulledActor(actor, speed));
|
||
}
|
||
|
||
list<PulledActor>& get_actors() { return actors; }
|
||
|
||
void set_stone(GridPos pos) { if (ystone) ystone->set_stone(pos); }
|
||
void dispose() { if (ystone) ystone->dispose(); }
|
||
};
|
||
|
||
|
||
class PullStone : public Stone, public TimeHandler {
|
||
// Variables.
|
||
enum State { IDLE, MOVING, VANISHED } state;
|
||
Direction m_movedir;
|
||
PullInfo *pull_info; // information about moved objects (only valid during pull)
|
||
|
||
public:
|
||
PullStone();
|
||
~PullStone();
|
||
|
||
private:
|
||
// Object interface.
|
||
PullStone *clone();
|
||
void dispose();
|
||
|
||
// Stone interface.
|
||
bool is_movable () const {
|
||
return state == IDLE;
|
||
}
|
||
void actor_hit(const StoneContact &sc) {
|
||
if (state == IDLE)
|
||
maybe_push_stone(sc);
|
||
}
|
||
void on_impulse(const Impulse& impulse);
|
||
bool is_removable() const {
|
||
return state == IDLE;
|
||
}
|
||
|
||
// TimeHandler interface.
|
||
void alarm();
|
||
|
||
// Functions.
|
||
void change_state(State new_state);
|
||
void set_move_state(bool appearing, Direction move_dir);
|
||
|
||
};
|
||
}
|
||
|
||
PullStone::PullStone()
|
||
: Stone("st-pull"), state(IDLE), m_movedir(NODIR) , pull_info(0)
|
||
{}
|
||
|
||
PullStone::~PullStone() {
|
||
GameTimer.remove_alarm(this);
|
||
delete pull_info;
|
||
}
|
||
|
||
PullStone *PullStone::clone() {
|
||
PullStone *other = new PullStone(*this);
|
||
other->pull_info = 0;
|
||
return other;
|
||
}
|
||
|
||
void PullStone::dispose() {
|
||
if (state == MOVING && pull_info != 0)
|
||
pull_info->dispose();
|
||
delete this;
|
||
}
|
||
|
||
void PullStone::set_move_state (bool appearing, Direction move_dir) {
|
||
if (appearing) {
|
||
m_movedir = move_dir;
|
||
// only the half-stone on the new field gets an alarm
|
||
// wherein it completes the move
|
||
GameTimer.set_alarm(this, 0.09, false);
|
||
}
|
||
else
|
||
m_movedir = reverse(move_dir);
|
||
change_state(MOVING);
|
||
}
|
||
|
||
void PullStone::change_state (State new_state) {
|
||
switch (new_state) {
|
||
case IDLE: set_model("st-pull"); break;
|
||
case MOVING: {
|
||
string mname = string("st-pull") + to_suffix(m_movedir);
|
||
set_model(mname);
|
||
break;
|
||
}
|
||
case VANISHED: break;
|
||
}
|
||
state = new_state;
|
||
}
|
||
|
||
void PullStone::alarm() {
|
||
ASSERT(state == MOVING, XLevelRuntime, "PullStone: alarm called with inconsistent state");
|
||
GridPos oldpos = move (get_pos(), reverse(m_movedir));
|
||
|
||
// remove the disappearing half of the PullStone :
|
||
PullStone *oldStone = dynamic_cast<PullStone*>(GetStone(oldpos));
|
||
ASSERT(oldStone, XLevelRuntime, "PullStone: oldStone non-existent in alarm");
|
||
oldStone->change_state(VANISHED);
|
||
KillStone(oldpos);
|
||
|
||
if (pull_info) { // have other objects been moved ?
|
||
pull_info->set_stone(oldpos); // re-sets any pulled stone
|
||
|
||
// set pulled actor(s):
|
||
list<PulledActor>::iterator e = pull_info->get_actors().end();
|
||
for (list<PulledActor>::iterator i = pull_info->get_actors().begin(); i != e; ++i) {
|
||
PulledActor& pulled = *i;
|
||
pulled.actor->get_actorinfo()->vel = pulled.speed;
|
||
}
|
||
delete pull_info;
|
||
pull_info = 0;
|
||
}
|
||
|
||
change_state(IDLE);
|
||
}
|
||
|
||
void PullStone::on_impulse(const Impulse& impulse)
|
||
{
|
||
if (state != IDLE)
|
||
return;
|
||
|
||
Direction move_dir = reverse(impulse.dir);
|
||
const GridPos& oldPos = get_pos();
|
||
GridPos newPos = move(oldPos, move_dir);
|
||
Stone *other_stone = GetStone(newPos);
|
||
|
||
if (other_stone &&
|
||
(!other_stone->is_removable() || IsLevelBorder(newPos))) {
|
||
return; // avoid unremoveable and border stones
|
||
}
|
||
|
||
PullStone *newStone = this->clone();
|
||
|
||
if (other_stone) {
|
||
// yield other_stone:
|
||
newStone->pull_info = new PullInfo(other_stone);
|
||
}
|
||
else {
|
||
newStone->pull_info = new PullInfo(0);
|
||
}
|
||
|
||
// search for affected actors
|
||
vector<Actor*> found_actors;
|
||
const double range_one_field = 1.415; // approx. 1 field [ > sqrt(1+1) ]
|
||
GetActorsInRange(newPos.center(), range_one_field, found_actors);
|
||
vector<Actor*>::iterator e = found_actors.end();
|
||
for (vector<Actor*>::iterator i = found_actors.begin(); i != e; ++i) {
|
||
Actor *actor = *i;
|
||
ActorInfo *ai = actor->get_actorinfo();
|
||
GridPos actor_pos(ai->pos);
|
||
|
||
if (actor_pos == newPos) { // if the actor is in the dest field
|
||
V2 vel = ai->vel;
|
||
V2 mid_dest = ai->pos;
|
||
|
||
mid_dest[0] = ecl::Clamp<double> (mid_dest[0], oldPos.x+0.01, oldPos.x+0.99);
|
||
mid_dest[1] = ecl::Clamp<double> (mid_dest[1], oldPos.y+0.01, oldPos.y+0.99);
|
||
WarpActor(actor, mid_dest[0], mid_dest[1], false);
|
||
|
||
newStone->pull_info->add_actor(actor, vel);
|
||
}
|
||
}
|
||
|
||
SetStone(newPos, newStone);
|
||
newStone->set_move_state(true, move_dir);
|
||
set_move_state(false, move_dir);
|
||
|
||
sound_event("moveslow");
|
||
}
|
||
|
||
|
||
/* -------------------- Oneway stones -------------------- */
|
||
|
||
// These stone can only be passed in one direction.
|
||
|
||
namespace
|
||
{
|
||
class OneWayBase : public Stone {
|
||
protected:
|
||
OneWayBase(const char *kind, Direction dir);
|
||
|
||
void init_model();
|
||
virtual Value message(const string& msg, const Value &val);
|
||
|
||
void actor_hit (const StoneContact&);
|
||
StoneResponse collision_response(const StoneContact &sc);
|
||
bool is_floating() const { return true; }
|
||
|
||
Direction get_orientation() const {
|
||
return Direction(int_attrib("orientation"));
|
||
}
|
||
void set_orientation(Direction dir) {
|
||
set_attrib("orientation", Value(dir));
|
||
}
|
||
|
||
virtual bool actor_may_pass (Actor *a) = 0;
|
||
};
|
||
|
||
class OneWayStone : public OneWayBase {
|
||
public:
|
||
OneWayStone(Direction dir=SOUTH) : OneWayBase("st-oneway", dir) {}
|
||
private:
|
||
CLONEOBJ(OneWayStone);
|
||
virtual bool actor_may_pass (Actor * /*a*/) { return true; }
|
||
};
|
||
|
||
|
||
class OneWayStone_black : public OneWayBase {
|
||
public:
|
||
OneWayStone_black(Direction dir=SOUTH)
|
||
: OneWayBase("st-oneway_black",dir) {}
|
||
private:
|
||
CLONEOBJ(OneWayStone_black);
|
||
virtual bool actor_may_pass (Actor *a) {
|
||
return a->get_attrib("blackball") != 0;
|
||
}
|
||
void actor_hit (const StoneContact&) {
|
||
// do nothing if hit by actor
|
||
}
|
||
};
|
||
|
||
class OneWayStone_white : public OneWayBase {
|
||
public:
|
||
OneWayStone_white(Direction dir=SOUTH)
|
||
: OneWayBase("st-oneway_white", dir) {}
|
||
private:
|
||
CLONEOBJ(OneWayStone_white);
|
||
virtual bool actor_may_pass (Actor *a) {
|
||
return a->get_attrib("whiteball") != 0;
|
||
}
|
||
void actor_hit (const StoneContact&) {
|
||
// do nothing if hit by actor
|
||
}
|
||
};
|
||
}
|
||
|
||
OneWayBase::OneWayBase(const char *kind, Direction dir)
|
||
: Stone(kind)
|
||
{
|
||
set_orientation(dir);
|
||
}
|
||
|
||
void OneWayBase::init_model()
|
||
{
|
||
string mname = get_kind();
|
||
mname += to_suffix(get_orientation());
|
||
set_model (mname);
|
||
}
|
||
|
||
Value OneWayBase::message(const string& msg, const Value &val) {
|
||
if (msg == "direction" && val.get_type() == Value::DOUBLE) {
|
||
set_orientation(to_direction(val));
|
||
init_model();
|
||
}
|
||
else if (msg == "signal" || msg == "flip") {
|
||
Direction dir = get_orientation();
|
||
set_orientation(reverse(dir));
|
||
init_model();
|
||
}
|
||
return Value();
|
||
}
|
||
|
||
void OneWayBase::actor_hit(const StoneContact &sc) {
|
||
Direction o=get_orientation();
|
||
|
||
if (has_dir(contact_faces(sc), o)) {
|
||
if (player::WieldedItemIs (sc.actor, "it-magicwand")) {
|
||
set_orientation(reverse(o));
|
||
init_model();
|
||
}
|
||
}
|
||
}
|
||
|
||
StoneResponse OneWayBase::collision_response(const StoneContact &sc) {
|
||
DirectionBits dirs=contact_faces(sc);
|
||
Direction o=get_orientation();
|
||
|
||
if (!sc.actor->is_flying() && actor_may_pass(sc.actor))
|
||
return has_dir(dirs,o) ? STONE_REBOUND : STONE_PASS;
|
||
else
|
||
return STONE_REBOUND;
|
||
}
|
||
|
||
|
||
/* -------------------- BolderStone -------------------- */
|
||
|
||
/** \page st-bolder Bolder Stone
|
||
|
||
The bolder stone will move in one direction until another stone will
|
||
block. When hit with a magic wand, the bolder stone reverse its
|
||
direction. When hitting a blocking stone it can activate switches or
|
||
oxyd stones.
|
||
|
||
\subsection boldera Attributes
|
||
|
||
- \c direction \n NORTH, EAST, SOUTH, WEST
|
||
|
||
*/
|
||
namespace
|
||
{
|
||
class BolderStone : public Stone {
|
||
CLONEOBJ(BolderStone);
|
||
public:
|
||
BolderStone(Direction dir=NORTH)
|
||
: Stone("st-bolder"), state(IDLE)
|
||
{
|
||
set_attrib("direction", dir);
|
||
// do not use set_dir, because this will set the state to ACTIVE
|
||
}
|
||
|
||
private:
|
||
enum State {
|
||
ACTIVE, // may send trigger into direction
|
||
IDLE, // already sent trigger w/o success
|
||
FALLING // falling into abyss
|
||
} state;
|
||
|
||
|
||
Direction get_dir() const {
|
||
return static_cast<Direction>(int_attrib("direction"));
|
||
}
|
||
void set_dir(Direction d) {
|
||
if (d != get_dir())
|
||
state = ACTIVE; // if turned by it-magicwand -> allow triggering
|
||
set_attrib("direction", d);
|
||
}
|
||
|
||
void on_floor_change() {
|
||
Floor *fl = GetFloor(get_pos());
|
||
if (fl->is_kind("fl-abyss")) {
|
||
state = FALLING;
|
||
init_model();
|
||
}
|
||
}
|
||
|
||
bool have_obstacle (Direction dir) {
|
||
return GetStone(move(get_pos(), dir)) != 0;
|
||
}
|
||
|
||
void trigger_obstacle (Direction dir) {
|
||
if (Stone *st = GetStone(move(get_pos(), dir))) {
|
||
SendMessage(st, "trigger", Value(dir));
|
||
}
|
||
}
|
||
|
||
void on_move() {
|
||
state = ACTIVE;
|
||
trigger_obstacle(get_dir());
|
||
Stone::on_move();
|
||
}
|
||
|
||
// Stone interface.
|
||
void init_model() {
|
||
string mname = "st-bolder" + to_suffix(get_dir());
|
||
if (state == FALLING)
|
||
mname += "-fall-anim";
|
||
set_anim(mname);
|
||
}
|
||
|
||
void animcb() {
|
||
display::Model *m = get_model();
|
||
Direction dir = get_dir();
|
||
switch (state) {
|
||
case FALLING:
|
||
KillStone(get_pos());
|
||
// init_model();
|
||
break;
|
||
|
||
case IDLE:
|
||
if (!have_obstacle(dir)) {
|
||
state = ACTIVE;
|
||
trigger_obstacle(dir);
|
||
}
|
||
// if (Model *m = get_model())
|
||
m->restart();
|
||
// init_model();
|
||
break;
|
||
|
||
case ACTIVE: {
|
||
trigger_obstacle(dir);
|
||
if (!move_stone(dir)) {
|
||
// if (state == MOVING) // state may be FALLING
|
||
state = IDLE;
|
||
}
|
||
init_model();
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
bool is_movable() const { return state != FALLING; }
|
||
|
||
void actor_hit(const StoneContact &sc) {
|
||
if (player::WieldedItemIs (sc.actor, "it-magicwand")) {
|
||
set_dir(reverse(get_dir()));
|
||
init_model();
|
||
}
|
||
}
|
||
|
||
void on_laserhit(Direction) {
|
||
set_dir(reverse(get_dir()));
|
||
init_model();
|
||
|
||
// @@@ FIXME: the direction should only be inverted on NEW laserbeam
|
||
// not on every light-recalc. Need to use PhotoCell!
|
||
}
|
||
|
||
void on_impulse (const Impulse& impulse) {
|
||
if (state == FALLING)
|
||
return;
|
||
|
||
if (impulse.sender && impulse.sender->is_kind("st-rotator")) {
|
||
set_dir(impulse.dir);
|
||
}
|
||
init_model();
|
||
move_stone(impulse.dir);
|
||
}
|
||
|
||
virtual Value message(const string& msg, const Value &val) {
|
||
if (msg == "direction" && state != FALLING) {
|
||
set_dir (to_direction(val));
|
||
init_model();
|
||
}
|
||
return Value();
|
||
}
|
||
};
|
||
}
|
||
|
||
|
||
/* -------------------- BlockerStone -------------------- */
|
||
|
||
/** \page st-blocker Blocker Stone
|
||
|
||
The BlockerStone acts like a normal stone until it is hit by a
|
||
BolderStone. Then it shrinks and morphs into a 'Blocker' item.
|
||
|
||
*/
|
||
|
||
namespace
|
||
{
|
||
class BlockerStone : public Stone {
|
||
CLONEOBJ(BlockerStone);
|
||
public:
|
||
BlockerStone(bool solid)
|
||
: Stone(solid ? "st-blocker" : "st-blocker-growing"),
|
||
state(solid ? SOLID : GROWING)
|
||
{}
|
||
|
||
private:
|
||
enum State { SOLID, SHRINKING, GROWING } state;
|
||
|
||
void init_model() {
|
||
switch (state) {
|
||
case SOLID:
|
||
set_model("st-blocker");
|
||
break;
|
||
|
||
case SHRINKING:
|
||
set_anim("st-blocker-shrinking");
|
||
break;
|
||
|
||
case GROWING:
|
||
set_anim("st-blocker-growing");
|
||
break;
|
||
}
|
||
}
|
||
|
||
void change_state(State newState) {
|
||
if (state != newState) {
|
||
if (state == GROWING && newState == SHRINKING) {
|
||
state = SHRINKING;
|
||
get_model()->reverse();
|
||
}
|
||
else if (state == SHRINKING && newState == GROWING) {
|
||
state = GROWING;
|
||
get_model()->reverse();
|
||
}
|
||
else {
|
||
state = newState;
|
||
init_model();
|
||
if (newState == SOLID) {
|
||
set_attrib("kind", "st-blocker");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
void animcb() {
|
||
switch (state) {
|
||
case SHRINKING: {
|
||
Item *it = world::MakeItem("it-blocker-new");
|
||
world::SetItem(get_pos(), it);
|
||
TransferObjectName(this, it);
|
||
world::KillStone(get_pos());
|
||
break;
|
||
}
|
||
case GROWING:
|
||
change_state(SOLID);
|
||
break;
|
||
default :
|
||
ASSERT(0, XLevelRuntime,
|
||
"BlockerStone: animcb called with inconsistent state");
|
||
break;
|
||
}
|
||
}
|
||
|
||
virtual Value message(const string &msg, const Value &val) {
|
||
if (msg == "trigger" || msg == "openclose") {
|
||
if (state == SHRINKING) {
|
||
change_state(GROWING);
|
||
}
|
||
else {
|
||
change_state(SHRINKING);
|
||
}
|
||
}
|
||
else if (msg == "signal") {
|
||
int value = to_int(val);
|
||
// warning("received signal (value=%i)", value);
|
||
if (value) { // value == 1 -> shrink
|
||
if (state != SHRINKING)
|
||
change_state(SHRINKING);
|
||
}
|
||
else { // value == 0 -> grow
|
||
if (state == SHRINKING)
|
||
change_state(GROWING);
|
||
}
|
||
}
|
||
else if (msg == "open") { // aka "shrink"
|
||
if (state != SHRINKING)
|
||
change_state(SHRINKING);
|
||
}
|
||
else if (msg == "close") { // aka "grow"
|
||
if (state == SHRINKING)
|
||
change_state(GROWING);
|
||
}
|
||
return Value();
|
||
}
|
||
|
||
void actor_contact(Actor *a) {
|
||
if (state == GROWING) {
|
||
SendMessage(a, "shatter");
|
||
}
|
||
}
|
||
void actor_inside(Actor *a) {
|
||
if (state == GROWING) {
|
||
SendMessage(a, "shatter");
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
|
||
/* -------------------- Volcano -------------------- */
|
||
namespace
|
||
{
|
||
class VolcanoStone : public Stone {
|
||
CLONEOBJ(VolcanoStone);
|
||
public:
|
||
enum State {INACTIVE, ACTIVE, FINISHED, BREAKING};
|
||
VolcanoStone( State initstate=INACTIVE) : Stone("st-volcano"), state( initstate) {}
|
||
private:
|
||
enum State state;
|
||
|
||
void init_model() {
|
||
switch( state) {
|
||
case FINISHED:
|
||
case INACTIVE: set_model( "st-plain"); break;
|
||
case ACTIVE: set_anim( "st-farting"); break;
|
||
case BREAKING: set_anim("st-stone_break-anim"); break;
|
||
}
|
||
}
|
||
|
||
void animcb() {
|
||
if (state == ACTIVE) {
|
||
// Spread
|
||
GridPos p = get_pos();
|
||
if (DoubleRand(0, 1) > 0.7) spread (move(p, NORTH));
|
||
if (DoubleRand(0, 1) > 0.7) spread (move(p, EAST));
|
||
if (DoubleRand(0, 1) > 0.7) spread (move(p, SOUTH));
|
||
if (DoubleRand(0, 1) > 0.7) spread (move(p, WEST));
|
||
|
||
// Be finished at random time
|
||
if (DoubleRand(0, 1) > 0.95)
|
||
state = FINISHED;
|
||
init_model();
|
||
} else if( state == BREAKING) {
|
||
KillStone( get_pos());
|
||
}
|
||
}
|
||
|
||
virtual Value message(const string &msg, const Value &) {
|
||
if (msg == "trigger") {
|
||
if (state == INACTIVE) {
|
||
state = ACTIVE;
|
||
init_model();
|
||
}
|
||
}
|
||
return Value();
|
||
}
|
||
|
||
void spread( GridPos p) {
|
||
Stone *st = GetStone(p);
|
||
if( !st) {
|
||
Item *it = MakeItem("it-seed_volcano");
|
||
SetItem( p, it);
|
||
SendMessage( it, "grow");
|
||
}
|
||
}
|
||
|
||
void actor_hit(const StoneContact &sc) {
|
||
Actor *a = sc.actor;
|
||
|
||
if( state == ACTIVE && player::WieldedItemIs (a, "it-hammer")) {
|
||
state = BREAKING;
|
||
init_model();
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
|
||
/* -------------------- ConnectiveStone -------------------- */
|
||
|
||
// base class for PuzzleStone and BigBrick
|
||
|
||
namespace {
|
||
class ConnectiveStone : public Stone {
|
||
public:
|
||
ConnectiveStone(const char *kind, int connections)
|
||
: Stone(kind)
|
||
{
|
||
set_attrib("connections", connections);
|
||
}
|
||
|
||
DirectionBits get_connections() const;
|
||
protected:
|
||
void init_model();
|
||
private:
|
||
virtual int get_modelno() const;
|
||
};
|
||
}
|
||
|
||
DirectionBits
|
||
ConnectiveStone::get_connections() const
|
||
{
|
||
int conn=int_attrib("connections") - 1;
|
||
if (conn >=0 && conn <16)
|
||
return DirectionBits(conn);
|
||
else
|
||
return NODIRBIT;
|
||
}
|
||
|
||
void ConnectiveStone::init_model() {
|
||
set_model(get_kind()+ecl::strf("%d", get_modelno()));
|
||
}
|
||
|
||
int ConnectiveStone::get_modelno() const {
|
||
return int_attrib("connections");
|
||
}
|
||
|
||
|
||
/* -------------------- BigBrick -------------------- */
|
||
|
||
// BigBricks allow to build stones of any size
|
||
|
||
namespace
|
||
{
|
||
class BigBrick : public ConnectiveStone {
|
||
CLONEOBJ(BigBrick);
|
||
public:
|
||
BigBrick(int connections)
|
||
: ConnectiveStone("st-bigbrick", connections)
|
||
{}
|
||
bool is_removable() const { return false; }
|
||
};
|
||
}
|
||
|
||
/* -------------------- BigBlueSand -------------------- */
|
||
|
||
// Same as BigBrick but with st-blue-sand
|
||
|
||
namespace
|
||
{
|
||
class BigBlueSand : public ConnectiveStone {
|
||
CLONEOBJ(BigBlueSand);
|
||
public:
|
||
BigBlueSand(int connections)
|
||
: ConnectiveStone("st-bigbluesand", connections)
|
||
{}
|
||
bool is_removable() const { return false; }
|
||
};
|
||
}
|
||
|
||
|
||
/* -------------------- Puzzle stones -------------------- */
|
||
|
||
/** \page st-puzzle Puzzle Stone
|
||
|
||
Puzzle stones can be connected to other stones of the same type. Any
|
||
of the four faces of the stone can have ``socket''. If the adjoining
|
||
faces of two neighboring stones both have a socket, the two stones
|
||
link up and henceforth move as group.
|
||
|
||
A cluster of puzzle stones may for example look like this:
|
||
|
||
\verbatim
|
||
+---+---+---+---+
|
||
| | | | |
|
||
| --+-+-+---+-+ |
|
||
| | | | | | |
|
||
+---+-+-+---+-+-+
|
||
| | | | | |
|
||
| | | | | |
|
||
| | | |
|
||
+---+ +---+
|
||
\endverbatim
|
||
|
||
This example actually presents the special case of a ``complete''
|
||
cluster. A cluster is complete if none of its stones has an
|
||
unconnected socket.
|
||
|
||
When touched with a magic wand the puzzle stones rotate
|
||
row- or columnwise.
|
||
|
||
\subsection puzzlea Attributes
|
||
|
||
- \b connections
|
||
number between 1 an 16. Each bit in (connections-1) corresponds to
|
||
a socket on one of the four faces. You will normally simply use
|
||
one of the Lua constants \c PUZ_0000 to \c PUZ_1111.
|
||
|
||
- \b oxyd
|
||
If 1 then the puzzle stones act oxyd-compatible: Complete clusters
|
||
explode, when they get touched. All other puzzle stones rotate row-
|
||
or columnwise. Groups of oxyd-compatible puzzle stones are shuffled
|
||
randomly at level startup.
|
||
|
||
\subsection puzzlee Example
|
||
<table>
|
||
<tr>
|
||
<td> \image html st-puzzletempl_0001.png "PUZ_0000"
|
||
<td> \image html st-puzzletempl_0002.png "PUZ_0001"
|
||
<td> \image html st-puzzletempl_0003.png "PUZ_0010"
|
||
<td> \image html st-puzzletempl_0004.png "PUZ_0011"
|
||
<tr>
|
||
<td> \image html st-puzzletempl_0005.png "PUZ_0100"
|
||
<td> \image html st-puzzletempl_0006.png "PUZ_0101"
|
||
<td> \image html st-puzzletempl_0007.png "PUZ_0110"
|
||
<td> \image html st-puzzletempl_0008.png "PUZ_0111"
|
||
<tr>
|
||
<td> \image html st-puzzletempl_0009.png "PUZ_1000"
|
||
<td> \image html st-puzzletempl_0010.png "PUZ_1001"
|
||
<td> \image html st-puzzletempl_0011.png "PUZ_1010"
|
||
<td> \image html st-puzzletempl_0012.png "PUZ_1011"
|
||
<tr>
|
||
<td> \image html st-puzzletempl_0013.png "PUZ_1100"
|
||
<td> \image html st-puzzletempl_0014.png "PUZ_1101"
|
||
<td> \image html st-puzzletempl_0015.png "PUZ_1110"
|
||
<td> \image html st-puzzletempl_0016.png "PUZ_1111"
|
||
</tr>
|
||
</table>
|
||
*/
|
||
namespace
|
||
{
|
||
class PuzzleStone : public ConnectiveStone, public TimeHandler, public world::PhotoCell {
|
||
INSTANCELISTOBJ(PuzzleStone);
|
||
public:
|
||
PuzzleStone(int connections, bool oxyd1_compatible_);
|
||
protected:
|
||
virtual ~PuzzleStone() {
|
||
GameTimer.remove_alarm (this);
|
||
}
|
||
private:
|
||
typedef vector<GridPos> Cluster;
|
||
|
||
/* ---------- Private methods ---------- */
|
||
|
||
bool oxyd1_compatible() const { return int_attrib("oxyd") != 0; }
|
||
|
||
static bool visit_dir(vector<GridPos> &stack, GridPos curpos,
|
||
Direction dir, int wanted_oxyd_attrib);
|
||
static void visit_adjacent(vector<GridPos>& stack, GridPos curpos,
|
||
Direction dir, int wanted_oxyd_attrib);
|
||
|
||
bool find_cluster(Cluster &);
|
||
void find_adjacents(Cluster &);
|
||
void find_row_or_column_cluster(Cluster &c, GridPos startpos,
|
||
Direction dir, int wanted_oxyd_attrib);
|
||
|
||
bool cluster_complete();
|
||
bool can_move_cluster (Cluster &c, Direction dir);
|
||
void maybe_move_cluster(Cluster &c, bool is_complete, bool actor_with_wand,
|
||
Direction dir);
|
||
void rotate_cluster(const Cluster &c);
|
||
void maybe_rotate_cluster(Direction dir);
|
||
|
||
int get_modelno() const;
|
||
|
||
void trigger_explosion(double delay);
|
||
static void trigger_explosion_at(GridPos p, double delay, int wanted_oxyd_attrib);
|
||
void explode();
|
||
bool explode_complete_cluster();
|
||
|
||
/* ---------- TimeHandler interface ---------- */
|
||
|
||
void alarm();
|
||
|
||
/* ---------- PhotoCell interface ---------- */
|
||
|
||
void on_recalc_start();
|
||
void on_recalc_finish();
|
||
|
||
/* ---------- Stone interface ---------- */
|
||
|
||
virtual Value message(const string& msg, const Value &val);
|
||
|
||
void on_creation (GridPos p);
|
||
void on_removal (GridPos p);
|
||
void on_impulse (const Impulse& impulse);
|
||
void on_laserhit (Direction dir);
|
||
|
||
bool is_floating() const;
|
||
|
||
StoneResponse collision_response(const StoneContact &sc);
|
||
void actor_hit (const StoneContact &sc);
|
||
void actor_contact (Actor *a);
|
||
|
||
/* ---------- Variables ---------- */
|
||
bool visited; // flag for DFS
|
||
enum { IDLE, EXPLODING } state;
|
||
DirectionBits illumination; // last state of surrounding laser beams
|
||
};
|
||
}
|
||
|
||
PuzzleStone::InstanceList PuzzleStone::instances;
|
||
|
||
PuzzleStone::PuzzleStone(int connections, bool oxyd1_compatible_)
|
||
: ConnectiveStone("st-puzzle", connections),
|
||
state (IDLE),
|
||
illumination (NODIRBIT)
|
||
{
|
||
set_attrib("oxyd", int(oxyd1_compatible_));
|
||
}
|
||
|
||
|
||
bool PuzzleStone::visit_dir(vector<GridPos> &stack, GridPos curpos,
|
||
Direction dir, int wanted_oxyd_attrib)
|
||
{
|
||
GridPos newpos = move(curpos, dir);
|
||
PuzzleStone *pz = dynamic_cast<PuzzleStone*>(GetStone(newpos));
|
||
|
||
if ((!pz) || (wanted_oxyd_attrib != pz->int_attrib("oxyd")))
|
||
return false;
|
||
|
||
DirectionBits cfaces = pz->get_connections();
|
||
|
||
if (cfaces==NODIRBIT || has_dir(cfaces, reverse(dir))) {
|
||
// Puzzle stone at newpos is connected to stone at curpos
|
||
if (!pz->visited) {
|
||
pz->visited = true;
|
||
stack.push_back(newpos);
|
||
}
|
||
return true;
|
||
} else {
|
||
// The two stones are adjacent but not connected
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/* Use a depth first search to determine the group of all stones that
|
||
are connected to the current stone. Returns true if the cluster is
|
||
``complete'' in the sense defined above. */
|
||
bool PuzzleStone::find_cluster(Cluster &cluster) {
|
||
for (unsigned i=0; i<instances.size(); ++i)
|
||
instances[i]->visited=false;
|
||
|
||
vector<GridPos> pos_stack;
|
||
bool is_complete = true;
|
||
pos_stack.push_back(get_pos());
|
||
this->visited = true;
|
||
int wanted_oxyd_attrib = int_attrib("oxyd");
|
||
|
||
while (!pos_stack.empty())
|
||
{
|
||
GridPos curpos = pos_stack.back();
|
||
pos_stack.pop_back();
|
||
|
||
PuzzleStone *pz = dynamic_cast<PuzzleStone*>(GetStone(curpos));
|
||
ASSERT(pz, XLevelRuntime, "PuzzleStone: missing stone in find_cluster");
|
||
|
||
cluster.push_back(curpos);
|
||
DirectionBits cfaces = pz->get_connections();
|
||
|
||
if (cfaces==NODIRBIT)
|
||
cfaces = DirectionBits(NORTHBIT | SOUTHBIT | EASTBIT | WESTBIT);
|
||
|
||
if (has_dir(cfaces, NORTH))
|
||
is_complete &= visit_dir(pos_stack, curpos, NORTH, wanted_oxyd_attrib);
|
||
if (has_dir(cfaces, EAST))
|
||
is_complete &= visit_dir(pos_stack, curpos, EAST, wanted_oxyd_attrib);
|
||
if (has_dir(cfaces, SOUTH))
|
||
is_complete &= visit_dir(pos_stack, curpos, SOUTH, wanted_oxyd_attrib);
|
||
if (has_dir(cfaces, WEST))
|
||
is_complete &= visit_dir(pos_stack, curpos, WEST, wanted_oxyd_attrib);
|
||
}
|
||
return is_complete;
|
||
}
|
||
|
||
void PuzzleStone::visit_adjacent (vector<GridPos>& stack, GridPos curpos,
|
||
Direction dir, int wanted_oxyd_attrib)
|
||
{
|
||
GridPos newpos = move(curpos, dir);
|
||
if (PuzzleStone *pz = dynamic_cast<PuzzleStone*>(GetStone(newpos))) {
|
||
if (!pz->visited) {
|
||
if (wanted_oxyd_attrib == pz->int_attrib("oxyd")) {
|
||
pz->visited = true;
|
||
stack.push_back(newpos);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* Use a depth first search to determine the group of all puzzle stones
|
||
with the same "oxyd" attrib that are adjacent to the current stone
|
||
(or to any other member of the group).
|
||
*/
|
||
void PuzzleStone::find_adjacents(Cluster &cluster) {
|
||
for (unsigned i=0; i<instances.size(); ++i)
|
||
instances[i]->visited=false;
|
||
|
||
vector<GridPos> pos_stack;
|
||
pos_stack.push_back(get_pos());
|
||
this->visited = true;
|
||
|
||
int wanted_oxyd_attrib = int_attrib("oxyd");
|
||
|
||
while (!pos_stack.empty()) {
|
||
GridPos curpos = pos_stack.back();
|
||
pos_stack.pop_back();
|
||
|
||
cluster.push_back(curpos);
|
||
visit_adjacent(pos_stack, curpos, NORTH, wanted_oxyd_attrib);
|
||
visit_adjacent(pos_stack, curpos, SOUTH, wanted_oxyd_attrib);
|
||
visit_adjacent(pos_stack, curpos, EAST, wanted_oxyd_attrib);
|
||
visit_adjacent(pos_stack, curpos, WEST, wanted_oxyd_attrib);
|
||
}
|
||
}
|
||
|
||
/* searches from 'startpos' into 'dir' for puzzle-stones.
|
||
wanted_oxyd_attrib == -1 -> take any puzzle stone
|
||
else -> take only puzzle stones of same type
|
||
*/
|
||
|
||
void PuzzleStone::find_row_or_column_cluster(Cluster &c, GridPos startpos,
|
||
Direction dir, int wanted_oxyd_attrib)
|
||
{
|
||
ASSERT(dir != NODIR, XLevelRuntime,
|
||
"PuzzleStone: no direction in find_row_or_column_cluster");
|
||
|
||
GridPos p = startpos;
|
||
while (Stone *puzz = dynamic_cast<PuzzleStone*>(GetStone(p))) {
|
||
if (wanted_oxyd_attrib != -1 && wanted_oxyd_attrib != puzz->int_attrib("oxyd"))
|
||
break; // stop when an unrequested puzzle stone type is readed
|
||
c.push_back(p);
|
||
p.move(dir);
|
||
}
|
||
}
|
||
|
||
bool PuzzleStone::can_move_cluster (Cluster &c, Direction dir)
|
||
{
|
||
sort(c.begin(), c.end());
|
||
Cluster mc(c); // Moved cluster
|
||
Cluster diff; // Difference between mc and c
|
||
|
||
for (unsigned i=0; i<mc.size(); ++i)
|
||
mc[i].move(dir);
|
||
|
||
set_difference(mc.begin(), mc.end(), c.begin(), c.end(),
|
||
back_inserter(diff));
|
||
|
||
// Now check whether all stones can be placed at their new
|
||
// position
|
||
bool move_ok = true;
|
||
for (unsigned i=0; i<diff.size(); ++i)
|
||
if (GetStone(diff[i]) != 0)
|
||
move_ok = false;
|
||
|
||
return move_ok;
|
||
}
|
||
|
||
void PuzzleStone::maybe_move_cluster(Cluster &c, bool is_complete,
|
||
bool actor_with_wand, Direction dir)
|
||
{
|
||
sort(c.begin(), c.end());
|
||
Cluster mc(c); // Moved cluster
|
||
Cluster diff; // Difference between mc and c
|
||
|
||
for (unsigned i=0; i<mc.size(); ++i)
|
||
mc[i].move(dir);
|
||
|
||
set_difference(mc.begin(), mc.end(), c.begin(), c.end(),
|
||
back_inserter(diff));
|
||
|
||
// Now check whether all stones can be placed at their new
|
||
// position
|
||
bool move_ok = true;
|
||
for (unsigned i=0; i<diff.size(); ++i)
|
||
if (GetStone(diff[i]) != 0)
|
||
move_ok = false;
|
||
|
||
if (!move_ok)
|
||
return;
|
||
|
||
// If the floor at a complete cluster's new position consists
|
||
// exclusively of abyss or water, create a bridge instead of
|
||
// moving the cluster.
|
||
//
|
||
// For partial clusters build bridges only on water and if the
|
||
// wielded item is NOT the magic wand.
|
||
|
||
bool create_bridge = true;
|
||
|
||
for (unsigned i=0; create_bridge && i<mc.size(); ++i) {
|
||
if (Floor *fl = GetFloor(mc[i])) {
|
||
if (fl->is_kind("fl-abyss")) {
|
||
if (!is_complete)
|
||
create_bridge = false;
|
||
}
|
||
else if (fl->is_kind("fl-water")) {
|
||
if (!is_complete && actor_with_wand)
|
||
create_bridge = false;
|
||
}
|
||
else
|
||
create_bridge = false;
|
||
}
|
||
}
|
||
|
||
// Finally, either move the whole cluster or create a bridge
|
||
sound_event("movebig");
|
||
if (create_bridge) {
|
||
for (unsigned i=0; i<c.size(); ++i) {
|
||
KillStone(c[i]);
|
||
SetFloor(mc[i], MakeFloor("fl-gray"));
|
||
}
|
||
}
|
||
else {
|
||
vector<Stone*> clusterstones;
|
||
for (unsigned i=0; i<c.size(); ++i)
|
||
clusterstones.push_back(YieldStone(c[i]));
|
||
|
||
for (unsigned i=0; i<c.size(); ++i) {
|
||
SetStone(mc[i], clusterstones[i]);
|
||
clusterstones[i]->on_move();
|
||
}
|
||
}
|
||
|
||
server::IncMoveCounter (static_cast<int> (c.size()));
|
||
}
|
||
|
||
bool PuzzleStone::cluster_complete() {
|
||
Cluster c;
|
||
return find_cluster(c);
|
||
}
|
||
|
||
int PuzzleStone::get_modelno() const {
|
||
int modelno = int_attrib("connections");
|
||
if (oxyd1_compatible()) modelno += 16;
|
||
return modelno;
|
||
}
|
||
|
||
void PuzzleStone::rotate_cluster(const Cluster &c) {
|
||
size_t size = c.size();
|
||
if (size > 1) {
|
||
int cn = GetStone(c[size-1])->int_attrib("connections");
|
||
for (size_t i=size-1; i>0; --i) {
|
||
PuzzleStone *st = dynamic_cast<PuzzleStone*> (GetStone (c[i]));
|
||
st->set_attrib ("connections", GetStone(c[i-1])->int_attrib ("connections"));
|
||
st->init_model();
|
||
}
|
||
GetStone(c[0])->set_attrib ("connections", cn);
|
||
dynamic_cast<PuzzleStone*> (GetStone(c[0]))->init_model();
|
||
}
|
||
}
|
||
|
||
StoneResponse PuzzleStone::collision_response(const StoneContact &/*sc*/) {
|
||
if (get_connections() == NODIRBIT)
|
||
return STONE_PASS;
|
||
return STONE_REBOUND;
|
||
}
|
||
|
||
void PuzzleStone::trigger_explosion(double delay) {
|
||
if (state == IDLE) {
|
||
state = EXPLODING;
|
||
GameTimer.set_alarm(this, delay, false);
|
||
}
|
||
}
|
||
|
||
void PuzzleStone::trigger_explosion_at (GridPos p, double delay,
|
||
int wanted_oxyd_attrib)
|
||
{
|
||
PuzzleStone *puzz = dynamic_cast<PuzzleStone*>(GetStone(p));
|
||
if (puzz && wanted_oxyd_attrib == puzz->int_attrib("oxyd")) {
|
||
// explode adjacent puzzle stones of same type
|
||
puzz->trigger_explosion(delay);
|
||
}
|
||
}
|
||
|
||
void PuzzleStone::explode() {
|
||
GridPos p = get_pos();
|
||
int ox_attr = int_attrib("oxyd");
|
||
|
||
// exchange puzzle stone with explosion
|
||
sound_event("stonedestroy");
|
||
SetStone(p, MakeStone("st-explosion"));
|
||
|
||
// trigger all adjacent puzzle stones :
|
||
const double DEFAULT_DELAY = 0.2;
|
||
trigger_explosion_at(move(p, NORTH), DEFAULT_DELAY, ox_attr);
|
||
trigger_explosion_at(move(p, SOUTH), DEFAULT_DELAY, ox_attr);
|
||
trigger_explosion_at(move(p, EAST), DEFAULT_DELAY, ox_attr);
|
||
trigger_explosion_at(move(p, WEST), DEFAULT_DELAY, ox_attr);
|
||
|
||
// @@@ FIXME: At the moment it's possible to push partial puzzle stones
|
||
// next to an already exploding cluster. Then the part will explode as well.
|
||
// Possible fix : mark whole cluster as "EXPLODING_SOON" when explosion is initiated
|
||
|
||
// ignite adjacent fields
|
||
// SendExplosionEffect(p, DYNAMITE);
|
||
}
|
||
|
||
void PuzzleStone::alarm() {
|
||
explode();
|
||
}
|
||
|
||
Value PuzzleStone::message(const string& msg, const Value &val) {
|
||
if (msg == "scramble") {
|
||
// oxyd levels contain explicit information on how to
|
||
// scramble puzzle stones. According to that information
|
||
// a "scramble" message is send to specific puzzle stones
|
||
// together with information about the direction.
|
||
//
|
||
// enigma levels may create scramble messages using
|
||
// AddScramble() and SetScrambleIntensity()
|
||
|
||
Direction dir = to_direction(val);
|
||
Cluster c;
|
||
find_row_or_column_cluster(c, get_pos(), dir, oxyd1_compatible());
|
||
|
||
size_t size = c.size();
|
||
|
||
// warning("received 'scramble'. dir=%s size=%i", to_suffix(dir).c_str(), size);
|
||
|
||
if (size >= 2) {
|
||
int count = IntegerRand(0, static_cast<int> (size-1));
|
||
while (count--)
|
||
rotate_cluster(c);
|
||
}
|
||
else {
|
||
warning("useless scramble (cluster size=%i)", size);
|
||
}
|
||
}
|
||
return Value();
|
||
}
|
||
|
||
void PuzzleStone::on_impulse(const Impulse& impulse)
|
||
{
|
||
// if (!oxyd1_compatible() && state == IDLE) {
|
||
if (state == IDLE) {
|
||
Cluster c;
|
||
bool is_complete = find_cluster(c);
|
||
bool actor_with_wand = false;
|
||
|
||
if (Actor *ac = dynamic_cast<Actor*>(impulse.sender))
|
||
actor_with_wand = player::WieldedItemIs (ac, "it-magicwand");
|
||
|
||
maybe_move_cluster(c, is_complete, actor_with_wand, impulse.dir);
|
||
}
|
||
}
|
||
|
||
bool PuzzleStone::explode_complete_cluster()
|
||
{
|
||
// @@@ FIXME: explode_complete_cluster should mark the whole cluster
|
||
// as "EXPLODING_SOON" (otherwise it may be changed before it explodes completely)
|
||
|
||
ASSERT(state == IDLE, XLevelRuntime,
|
||
"PuzzleStone: explode_complete_cluster called with inconsistent state");
|
||
bool exploded = false;
|
||
|
||
Cluster complete;
|
||
if (find_cluster(complete)) {
|
||
Cluster all;
|
||
find_adjacents(all);
|
||
|
||
// If all adjacent stones build one complete cluster explode it
|
||
if (all.size() == complete.size()) {
|
||
explode(); // explode complete cluster
|
||
exploded = true;
|
||
}
|
||
else {
|
||
ASSERT(all.size() > complete.size(), XLevelRuntime,
|
||
"PuzzleStone: sizes don't match in explode_complete_cluster");
|
||
if (!oxyd1_compatible()) {
|
||
// check if 'all' is made up of complete clusters :
|
||
|
||
sort(all.begin(), all.end());
|
||
|
||
while (1) {
|
||
sort(complete.begin(), complete.end());
|
||
|
||
// remove one complete cluster from 'all'
|
||
{
|
||
Cluster rest;
|
||
set_symmetric_difference(all.begin(), all.end(),
|
||
complete.begin(), complete.end(),
|
||
back_inserter(rest));
|
||
// now rest contains 'all' minus 'complete'
|
||
swap(all, rest);
|
||
}
|
||
|
||
if (all.empty()) { // none left -> all were complete
|
||
exploded = true;
|
||
break;
|
||
}
|
||
|
||
// look for next complete cluster :
|
||
complete.clear();
|
||
{
|
||
PuzzleStone *pz = dynamic_cast<PuzzleStone*>(GetStone(all[0]));
|
||
ASSERT(pz, XLevelRuntime,
|
||
"PuzzleStone: missing stone in explode_complete_cluster");
|
||
if (!pz->find_cluster(complete)) {
|
||
break; // incomplete cluster found -> don't explode
|
||
}
|
||
}
|
||
}
|
||
|
||
if (exploded) {
|
||
// warning("exploding complete cluster");
|
||
explode();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return exploded;
|
||
}
|
||
|
||
bool PuzzleStone::is_floating() const {
|
||
return get_connections() == 0;
|
||
}
|
||
|
||
void PuzzleStone::maybe_rotate_cluster(Direction dir)
|
||
{
|
||
if (dir != NODIR) {
|
||
Cluster c;
|
||
find_row_or_column_cluster(c, get_pos(), dir, int_attrib ("oxyd"));
|
||
if (c.size() >= 2) {
|
||
// warning("ok -> rotate");
|
||
rotate_cluster(c);
|
||
}
|
||
}
|
||
}
|
||
|
||
void PuzzleStone::on_creation (GridPos p) {
|
||
photo_activate();
|
||
ConnectiveStone::on_creation (p);
|
||
illumination = NODIRBIT;
|
||
}
|
||
|
||
void PuzzleStone::on_removal(GridPos p) {
|
||
photo_deactivate();
|
||
ConnectiveStone::on_removal(p);
|
||
}
|
||
|
||
void PuzzleStone::on_laserhit (Direction dir) {
|
||
ecl::set_flags (illumination, to_bits(reverse(dir)));
|
||
}
|
||
|
||
void PuzzleStone::on_recalc_start() {
|
||
illumination = NODIRBIT;
|
||
}
|
||
|
||
void PuzzleStone::on_recalc_finish() {
|
||
if (illumination != (ALL_DIRECTIONS+1) &&
|
||
illumination != NODIRBIT &&
|
||
state == IDLE)
|
||
{
|
||
if (!explode_complete_cluster() && oxyd1_compatible()) {
|
||
if (illumination & NORTHBIT) maybe_rotate_cluster(SOUTH);
|
||
if (illumination & SOUTHBIT) maybe_rotate_cluster(NORTH);
|
||
if (illumination & EASTBIT) maybe_rotate_cluster(WEST);
|
||
if (illumination & WESTBIT) maybe_rotate_cluster(EAST);
|
||
}
|
||
}
|
||
}
|
||
|
||
void PuzzleStone::actor_hit(const StoneContact &sc)
|
||
{
|
||
if (get_connections() == NODIRBIT)
|
||
return; // Puzzle stone is hollow
|
||
|
||
if (state == EXPLODING)
|
||
return;
|
||
|
||
Cluster c;
|
||
find_cluster (c);
|
||
|
||
Direction rotate_dir = reverse (contact_face (sc));
|
||
Direction move_dir = get_push_direction(sc);
|
||
|
||
if (oxyd1_compatible()) {
|
||
// Oxyd 1
|
||
|
||
if (explode_complete_cluster())
|
||
return;
|
||
|
||
// 1) If unconnected puzzle stones -> try to move it
|
||
if (c.size() == 1 && move_dir != NODIR) {
|
||
// if cluster contains single stone
|
||
// -> move it if dest pos is free
|
||
GridPos dest = move(c[0], move_dir);
|
||
if (GetStone(dest) == 0) {
|
||
Stone *puzz = YieldStone(c[0]);
|
||
SetStone(dest, puzz);
|
||
puzz->on_move();
|
||
sound_event ("movesmall");
|
||
} else
|
||
maybe_rotate_cluster (rotate_dir);
|
||
}
|
||
// 2) If more than one stone,
|
||
else
|
||
maybe_rotate_cluster (rotate_dir);
|
||
}
|
||
else {
|
||
// Not Oxyd 1
|
||
|
||
bool has_magic_wand = player::WieldedItemIs (sc.actor, "it-magicwand");
|
||
|
||
// 1) Try to start explosion of complete cluster
|
||
if (has_magic_wand && explode_complete_cluster())
|
||
return;
|
||
|
||
// 2) Failed? Try to move the cluster
|
||
if (move_dir != NODIR && can_move_cluster (c, move_dir)) {
|
||
sc.actor->send_impulse(get_pos(), move_dir);
|
||
return;
|
||
}
|
||
|
||
// 3) Last chance: try to rotate the row or column
|
||
if (has_magic_wand)
|
||
maybe_rotate_cluster (rotate_dir);
|
||
}
|
||
}
|
||
|
||
void PuzzleStone::actor_contact (Actor *a)
|
||
{
|
||
if (state == EXPLODING)
|
||
SendMessage (a, "shatter");
|
||
}
|
||
|
||
|
||
|
||
/* -------------------- DoorBase -------------------- */
|
||
|
||
// Base class for everything that behaves like a door, i.e., it has
|
||
// four states OPEN, CLOSED, OPENING, CLOSING.
|
||
namespace
|
||
{
|
||
class DoorBase : public Stone {
|
||
protected:
|
||
enum State { OPEN, CLOSED, OPENING, CLOSING } state;
|
||
|
||
DoorBase(const char *name, State initstate=CLOSED)
|
||
: Stone(name), state(initstate)
|
||
{}
|
||
|
||
State get_state() const { return state; }
|
||
void set_state(State st) { state=st; }
|
||
|
||
private:
|
||
// DoorBase interface
|
||
virtual string model_basename() { return get_kind(); }
|
||
virtual void init_model();
|
||
virtual string opening_sound() const { return ""; }
|
||
virtual string closing_sound() const { return ""; }
|
||
|
||
// Private methods
|
||
void change_state(State newstate) ;
|
||
virtual Value message(const string &m, const Value &);
|
||
|
||
StoneResponse collision_response(const StoneContact &sc);
|
||
|
||
void animcb();
|
||
|
||
// Stone interface
|
||
virtual bool is_transparent (Direction) const
|
||
{ return state==OPEN; }
|
||
|
||
virtual bool is_sticky (const Actor *) const
|
||
{ return false; }
|
||
};
|
||
}
|
||
|
||
Value DoorBase::message(const string &m, const Value &val) {
|
||
State newstate = state;
|
||
int ival = to_int (val);
|
||
|
||
if (m == "open")
|
||
newstate = OPENING;
|
||
else if (m == "close")
|
||
newstate = CLOSING;
|
||
else if (m == "openclose")
|
||
newstate = (state==OPEN || state==OPENING) ? CLOSING : OPENING;
|
||
else if (m == "signal")
|
||
newstate = ival > 0 ? OPENING : CLOSING;
|
||
|
||
if (newstate==OPENING && (state==CLOSED || state==CLOSING))
|
||
change_state(OPENING);
|
||
else if (newstate==CLOSING && (state==OPEN || state==OPENING))
|
||
change_state(CLOSING);
|
||
return Value();
|
||
}
|
||
|
||
void DoorBase::init_model() {
|
||
string mname = model_basename();
|
||
if (state == CLOSED)
|
||
mname += "-closed";
|
||
else if (state==OPEN)
|
||
mname += "-open";
|
||
set_model(mname);
|
||
}
|
||
|
||
void DoorBase::animcb() {
|
||
if (state == OPENING)
|
||
change_state(OPEN);
|
||
else if (state == CLOSING)
|
||
change_state(CLOSED);
|
||
}
|
||
|
||
StoneResponse
|
||
DoorBase::collision_response(const StoneContact &/*sc*/)
|
||
{
|
||
return (state == OPEN) ? STONE_PASS:STONE_REBOUND;
|
||
}
|
||
|
||
void DoorBase::change_state(State newstate)
|
||
{
|
||
string basename = model_basename();
|
||
|
||
switch (newstate) {
|
||
case OPEN:
|
||
set_model(basename+"-open");
|
||
lasers::MaybeRecalcLight(get_pos());
|
||
break;
|
||
case CLOSED:
|
||
set_model(basename+"-closed");
|
||
world::ShatterActorsInsideField (get_pos());
|
||
lasers::MaybeRecalcLight(get_pos()); // maybe superfluous
|
||
break;
|
||
case OPENING:
|
||
sound_event (opening_sound().c_str());
|
||
if (state == CLOSING)
|
||
get_model()->reverse();
|
||
else
|
||
set_anim(basename+"-opening");
|
||
break;
|
||
case CLOSING:
|
||
sound_event (closing_sound().c_str());
|
||
if (state == OPENING)
|
||
get_model()->reverse();
|
||
else
|
||
set_anim(basename+"-closing");
|
||
world::ShatterActorsInsideField (get_pos());
|
||
lasers::MaybeRecalcLight(get_pos());
|
||
break;
|
||
}
|
||
set_state(newstate);
|
||
}
|
||
|
||
|
||
/* -------------------- Door -------------------- */
|
||
|
||
// Attributes:
|
||
//
|
||
// :type h or v for a door that opens horizontally or vertically
|
||
namespace
|
||
{
|
||
class Door : public DoorBase {
|
||
CLONEOBJ(Door);
|
||
public:
|
||
Door(const char *type="h", bool open=false)
|
||
: DoorBase("st-door", open ? OPEN : CLOSED)
|
||
{
|
||
set_attrib("type", type);
|
||
}
|
||
private:
|
||
virtual string opening_sound() const { return "dooropen"; }
|
||
virtual string closing_sound() const { return "doorclose"; }
|
||
virtual const char *collision_sound() { return "electric"; }
|
||
string get_type() const {
|
||
string type="h";
|
||
string_attrib("type", &type);
|
||
return type;
|
||
}
|
||
|
||
bool is_transparent (Direction) const;
|
||
bool is_floating () const {
|
||
return true; // don't let door press buttons
|
||
}
|
||
|
||
void actor_hit(const StoneContact &)
|
||
{
|
||
if (Item *it = GetItem (get_pos()))
|
||
PerformAction (it, true);
|
||
}
|
||
|
||
string model_basename() { return string("st-door")+get_type(); }
|
||
StoneResponse collision_response(const StoneContact &sc);
|
||
};
|
||
|
||
class Door_a : public DoorBase {
|
||
CLONEOBJ(Door_a);
|
||
public:
|
||
Door_a() : DoorBase("st-door_a") {}
|
||
};
|
||
|
||
class Door_b : public DoorBase {
|
||
CLONEOBJ(Door_b);
|
||
public:
|
||
Door_b() : DoorBase("st-door_b") {}
|
||
};
|
||
|
||
class Door_c : public DoorBase {
|
||
CLONEOBJ(Door_c);
|
||
public:
|
||
Door_c() : DoorBase("st-door_c") {}
|
||
};
|
||
}
|
||
|
||
bool Door::is_transparent (Direction dir) const {
|
||
if (get_type() == "h")
|
||
return state==OPEN || dir==EAST || dir==WEST;
|
||
else
|
||
return state==OPEN || dir==NORTH || dir==SOUTH;
|
||
}
|
||
|
||
StoneResponse
|
||
Door::collision_response(const StoneContact &sc)
|
||
{
|
||
Direction cf = contact_face(sc);
|
||
if (state == OPEN)
|
||
return STONE_PASS;
|
||
else if (state == CLOSING)
|
||
return STONE_REBOUND;
|
||
else {
|
||
string t = get_type();
|
||
return ((t == "v" && (cf==WEST || cf==EAST)) ||
|
||
(t == "h" && (cf==SOUTH || cf==NORTH)))
|
||
? STONE_REBOUND
|
||
: STONE_PASS;
|
||
}
|
||
}
|
||
|
||
|
||
/* -------------------- ShogunStone -------------------- */
|
||
|
||
// Attributes:
|
||
//
|
||
// :holes 1..7
|
||
namespace
|
||
{
|
||
class ShogunStone : public Stone {
|
||
CLONEOBJ(ShogunStone);
|
||
|
||
enum Holes { SMALL = 1, MEDIUM = 2, LARGE = 4};
|
||
static Holes smallest_hole(Holes s);
|
||
void set_holes(Holes h) { set_attrib("holes", h); }
|
||
|
||
public:
|
||
ShogunStone(int holes=SMALL) : Stone("st-shogun") {
|
||
set_holes(static_cast<Holes>(holes));
|
||
}
|
||
private:
|
||
Holes get_holes() const;
|
||
void notify_item();
|
||
|
||
virtual Value message(const string &m, const Value &) {
|
||
if (m == "init") { // request from ShogunDot (if set _after_ ShogunStone)
|
||
notify_item();
|
||
}
|
||
return Value();
|
||
}
|
||
|
||
void add_hole(Holes h) {
|
||
set_attrib("holes", get_holes() | h);
|
||
notify_item();
|
||
init_model();
|
||
}
|
||
|
||
void on_creation (GridPos p) {
|
||
init_model();
|
||
notify_item();
|
||
}
|
||
|
||
void on_impulse(const Impulse& impulse);
|
||
|
||
void init_model() {
|
||
set_model(ecl::strf("st-shogun%d", int(get_holes())));
|
||
}
|
||
|
||
bool is_movable() const { return false; }
|
||
|
||
void actor_hit (const StoneContact &sc) {
|
||
maybe_push_stone (sc);
|
||
}
|
||
};
|
||
}
|
||
|
||
ShogunStone::Holes ShogunStone::get_holes() const {
|
||
int h=int_attrib("holes");
|
||
if (h>=1 && h<=7)
|
||
return Holes(h);
|
||
else {
|
||
warning("Wrong 'holes' attribute (%i)", h);
|
||
return SMALL;
|
||
}
|
||
}
|
||
|
||
ShogunStone::Holes ShogunStone::smallest_hole(Holes s) {
|
||
if (s & SMALL) return SMALL;
|
||
if (s & MEDIUM) return MEDIUM;
|
||
if (s & LARGE) return LARGE;
|
||
throw XLevelRuntime ("ShogunStone: internal error");
|
||
}
|
||
|
||
void ShogunStone::notify_item ()
|
||
{
|
||
if (Item *it = GetItem(get_pos())) {
|
||
switch (get_holes()) {
|
||
case SMALL: SendMessage(it, "shogun1"); break;
|
||
case (MEDIUM | SMALL): SendMessage(it, "shogun2"); break;
|
||
case (LARGE | MEDIUM | SMALL): SendMessage(it, "shogun3"); break;
|
||
default: SendMessage(it, "noshogun"); break;
|
||
}
|
||
}
|
||
}
|
||
|
||
void ShogunStone::on_impulse(const Impulse& impulse) {
|
||
GridPos destpos = move(get_pos(), impulse.dir);
|
||
Holes holes = get_holes();
|
||
Holes smallest = smallest_hole(holes);
|
||
ShogunStone *target = 0;
|
||
|
||
if (Stone *st = GetStone(destpos)) {
|
||
target = dynamic_cast<ShogunStone*>(st);
|
||
|
||
/* If the stone at `p' is not a shogun stone or if smallest hole
|
||
does not fit into target, do not transfer the smallest hole. */
|
||
if (!target || smallest >= smallest_hole(target->get_holes()))
|
||
return;
|
||
}
|
||
|
||
/* It's important to remove the old stone before setting the new
|
||
one: otherwise it is possible to activate two triggers with one
|
||
shogun stone when shifting from one shogun target to a second
|
||
adjacent shogun target. */
|
||
|
||
GridPos my_pos = get_pos();
|
||
string old_name;
|
||
|
||
// Remove/modify source stone:
|
||
if (Holes newholes = Holes(holes & ~smallest)) {
|
||
set_holes(newholes);
|
||
notify_item();
|
||
init_model();
|
||
}
|
||
else {
|
||
string_attrib("name", &old_name); // store name of disappearing stone
|
||
SendMessage(GetItem(my_pos), "noshogun");
|
||
KillStone(my_pos);
|
||
}
|
||
|
||
// Modify/create target stone:
|
||
if (target) {
|
||
target->add_hole(smallest);
|
||
// target->sound_event("st-magic");
|
||
// sound::PlaySound("st-magic", my_pos.center()); // object already disappeared
|
||
}
|
||
else { // create new
|
||
target = new ShogunStone(smallest);
|
||
SetStone(destpos, target);
|
||
target->on_move();
|
||
}
|
||
|
||
if (!old_name.empty())
|
||
NameObject(target, old_name);
|
||
|
||
server::IncMoveCounter();
|
||
sound::EmitSoundEvent ("movesmall", my_pos.center());
|
||
}
|
||
|
||
|
||
|
||
/* -------------------- Stone impulse stones -------------------- */
|
||
|
||
// Messages:
|
||
//
|
||
// :trigger
|
||
namespace
|
||
{
|
||
class StoneImpulse_Base : public Stone {
|
||
protected:
|
||
StoneImpulse_Base(const char *kind) : Stone(kind), state(IDLE), incoming(NODIR)
|
||
{}
|
||
|
||
enum State { IDLE, PULSING, CLOSING };
|
||
State state;
|
||
Direction incoming; // direction of incoming impulse (may be NODIR)
|
||
|
||
void change_state(State st);
|
||
|
||
virtual void on_impulse(const Impulse& impulse) {
|
||
incoming = impulse.dir;
|
||
change_state(PULSING);
|
||
}
|
||
|
||
private:
|
||
|
||
virtual void notify_state(State st) = 0;
|
||
|
||
virtual Value message(const string &m, const Value &value) {
|
||
if (m=="trigger") {
|
||
incoming = (value.get_type() == Value::DOUBLE)
|
||
? Direction( static_cast<int> (value.get_double()+0.1))
|
||
: NODIR;
|
||
|
||
change_state(PULSING);
|
||
}
|
||
else if (m == "signal" && to_double (value) != 0) {
|
||
incoming = NODIR;
|
||
change_state (PULSING);
|
||
}
|
||
return Value();
|
||
}
|
||
|
||
void animcb() {
|
||
if (state == PULSING)
|
||
change_state (CLOSING);
|
||
else if (state == CLOSING)
|
||
change_state (IDLE);
|
||
}
|
||
|
||
void on_laserhit(Direction dir) {
|
||
incoming = dir;
|
||
change_state(PULSING);
|
||
}
|
||
};
|
||
|
||
}
|
||
|
||
void StoneImpulse_Base::change_state(State new_state) {
|
||
if (new_state == state) return;
|
||
|
||
GridPos p = get_pos();
|
||
switch (new_state) {
|
||
case IDLE: {
|
||
state = new_state;
|
||
notify_state(state);
|
||
break;
|
||
}
|
||
case PULSING:
|
||
if (state != IDLE) {
|
||
return; // do not set new state
|
||
}
|
||
state = new_state;
|
||
notify_state(state);
|
||
sound_event("impulse");
|
||
break;
|
||
case CLOSING: {
|
||
GridPos targetpos[4];
|
||
bool haveStone[4];
|
||
|
||
// set CLOSING model _before_ sending impulses !!!
|
||
// (any impulse might have side effects that move this stone)
|
||
|
||
state = new_state;
|
||
notify_state(state);
|
||
|
||
for (int d = 0; d < 4; ++d) {
|
||
targetpos[d] = move(p, Direction(d));
|
||
haveStone[d] = GetStone(targetpos[d]) != 0;
|
||
}
|
||
|
||
for (int d = int(incoming)+1; d <= int(incoming)+4; ++d) {
|
||
int D = d%4;
|
||
if (haveStone[D]) {
|
||
send_impulse(targetpos[D], Direction(D));
|
||
}
|
||
}
|
||
|
||
incoming = NODIR; // forget impulse direction
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
namespace
|
||
{
|
||
class StoneImpulseStone : public StoneImpulse_Base {
|
||
CLONEOBJ(StoneImpulseStone);
|
||
public:
|
||
StoneImpulseStone() : StoneImpulse_Base("st-stoneimpulse")
|
||
{}
|
||
|
||
private:
|
||
void notify_state(State st) {
|
||
switch (st) {
|
||
case IDLE:
|
||
init_model();
|
||
break;
|
||
case PULSING:
|
||
set_anim("st-stoneimpulse-anim1");
|
||
break;
|
||
case CLOSING:
|
||
set_anim("st-stoneimpulse-anim2");
|
||
break;
|
||
}
|
||
}
|
||
|
||
void actor_hit(const StoneContact &/*sc*/) {
|
||
change_state(PULSING);
|
||
}
|
||
|
||
};
|
||
|
||
|
||
class HollowStoneImpulseStone : public StoneImpulse_Base {
|
||
CLONEOBJ(HollowStoneImpulseStone);
|
||
public:
|
||
HollowStoneImpulseStone()
|
||
: StoneImpulse_Base("st-stoneimpulse-hollow") {}
|
||
private:
|
||
void notify_state(State st) {
|
||
switch (st) {
|
||
case IDLE:
|
||
init_model();
|
||
lasers::MaybeRecalcLight(get_pos());
|
||
break;
|
||
case PULSING:
|
||
lasers::MaybeRecalcLight(get_pos());
|
||
set_anim("st-stoneimpulse-hollow-anim1");
|
||
break;
|
||
case CLOSING:
|
||
set_anim("st-stoneimpulse-hollow-anim2");
|
||
break;
|
||
}
|
||
}
|
||
|
||
StoneResponse collision_response(const StoneContact &/*sc*/) {
|
||
return (state == IDLE) ? STONE_PASS : STONE_REBOUND;
|
||
}
|
||
void actor_inside(Actor *a) {
|
||
if (state == PULSING || state == CLOSING)
|
||
SendMessage(a, "shatter");
|
||
}
|
||
|
||
bool is_floating () const {
|
||
return true;
|
||
}
|
||
|
||
void on_laserhit (Direction) {
|
||
// hollow StoneImpulseStones cannot be activated using lasers
|
||
}
|
||
};
|
||
|
||
|
||
class MovableImpulseStone : public StoneImpulse_Base {
|
||
CLONEOBJ(MovableImpulseStone);
|
||
public:
|
||
MovableImpulseStone()
|
||
: StoneImpulse_Base("st-stoneimpulse_movable"),
|
||
repulse(false)
|
||
{
|
||
}
|
||
|
||
private:
|
||
|
||
void notify_state(State st) {
|
||
switch (st) {
|
||
case IDLE:
|
||
if (repulse) {
|
||
repulse = false;
|
||
change_state(PULSING);
|
||
}
|
||
else
|
||
init_model();
|
||
break;
|
||
case PULSING:
|
||
set_anim("st-stoneimpulse-anim1");
|
||
break;
|
||
case CLOSING:
|
||
set_anim("st-stoneimpulse-anim2");
|
||
break;
|
||
}
|
||
}
|
||
|
||
void init_model() {
|
||
set_model("st-stoneimpulse");
|
||
}
|
||
|
||
// Stone interface:
|
||
|
||
void actor_hit(const StoneContact &sc) {
|
||
if (!maybe_push_stone (sc)) {
|
||
incoming = NODIR; // bad, but no real problem!
|
||
if (state == IDLE)
|
||
change_state(PULSING);
|
||
}
|
||
}
|
||
|
||
void on_impulse(const Impulse& impulse) {
|
||
State oldstate = state;
|
||
|
||
if (move_stone(impulse.dir)) {
|
||
notify_state(oldstate); // restart anim if it was animated before move
|
||
|
||
Actor *hitman = dynamic_cast<Actor*>(impulse.sender);
|
||
if (hitman && player::WieldedItemIs (hitman, "it-magicwand")) {
|
||
return; // do not change state to PULSING
|
||
}
|
||
}
|
||
|
||
if (state == IDLE)
|
||
change_state(PULSING);
|
||
}
|
||
|
||
void on_move() {
|
||
if (state != PULSING)
|
||
repulse = true; // pulse again
|
||
Stone::on_move();
|
||
}
|
||
|
||
bool is_movable() const {
|
||
// moving the stone is handled explicitly in actor_hit()
|
||
return false; //true;
|
||
}
|
||
|
||
// Variables.
|
||
bool repulse;
|
||
};
|
||
}
|
||
|
||
|
||
/* -------------------- Oxyd stone -------------------- */
|
||
|
||
/** \page st-oxyd Oxyd Stone
|
||
|
||
Oxyd stones are characterized by two attributes: Their flavor and
|
||
their color. The flavor only affects the visual representation of
|
||
the stone; it can be either 'a' (opening like a flower) or 'b'
|
||
(displaying a fade-in animation). The color attribute determines
|
||
the shape on the oxyd stone.
|
||
|
||
\b Note: You should usually not to create Oxyd stones manually
|
||
with \c set_stone(). Use the predefined \c oxyd() function instead.
|
||
|
||
\subsection oxyda Attributes
|
||
|
||
- \b flavor "a", "b", "c", or "d"
|
||
- \b color number between 0 and 7
|
||
|
||
\subsection oxydm Messages
|
||
|
||
- \b closeall close all oxyd stones
|
||
- \b shuffle interchange the colors of the oxyd stones in the current landscape
|
||
- \b trigger open the stone
|
||
|
||
<table><tr>
|
||
<td>\image html st-oxyda.png "flavor A"
|
||
<td>\image html st-oxydb.png "flavor B"
|
||
<td>\image html st-oxydc.png "flavor C"
|
||
<td>\image html st-oxydd.png "flavor D"
|
||
</table>
|
||
*/
|
||
|
||
namespace
|
||
{
|
||
class OxydStone : public PhotoStone {
|
||
INSTANCELISTOBJ(OxydStone);
|
||
public:
|
||
OxydStone();
|
||
|
||
static void shuffle_colors();
|
||
virtual bool is_removable() const;
|
||
private:
|
||
enum State { CLOSED, OPEN, OPENING, CLOSING, BLINKING };
|
||
State state;
|
||
|
||
// Stone interface
|
||
void actor_hit(const StoneContact &sc);
|
||
void on_creation (GridPos p);
|
||
void on_removal (GridPos p);
|
||
const char *collision_sound() { return "stone"; }
|
||
virtual Value message(const string &m, const Value &);
|
||
|
||
|
||
// PhotoStone interface
|
||
void notify_laseron() { maybe_open_stone(); }
|
||
void notify_laseroff() {}
|
||
|
||
// Animation callback
|
||
void animcb();
|
||
|
||
// Private methods
|
||
void maybe_open_stone();
|
||
void change_state(State newstate);
|
||
|
||
|
||
static bool blinking(OxydStone *a) {
|
||
return (a->state==BLINKING);
|
||
}
|
||
static bool blinking_or_opening(OxydStone *a) {
|
||
return (a->state==BLINKING || a->state == OPENING);
|
||
}
|
||
static bool not_open(OxydStone *a) {
|
||
return !(a->state==OPEN || a->state==OPENING);
|
||
}
|
||
|
||
};
|
||
}
|
||
|
||
OxydStone::InstanceList OxydStone::instances;
|
||
|
||
OxydStone::OxydStone()
|
||
: PhotoStone("st-oxyd"),
|
||
state(CLOSED)
|
||
{
|
||
set_attrib("flavor", "b");
|
||
set_attrib("color", "0");
|
||
}
|
||
|
||
Value OxydStone::message(const string &m, const Value &val)
|
||
{
|
||
if (m=="closeall") {
|
||
for (unsigned i=0; i<instances.size(); ++i)
|
||
instances[i]->change_state(CLOSING);
|
||
}
|
||
else if (m=="shuffle") {
|
||
shuffle_colors();
|
||
}
|
||
else if (m=="trigger" || m=="spitter") {
|
||
maybe_open_stone();
|
||
}
|
||
else if (m=="signal" && to_int(val) != 0) {
|
||
maybe_open_stone();
|
||
}
|
||
else if (m=="init") {
|
||
// odd number of oxyd stones in the level? no problem, turn a
|
||
// random one into a fake oxyd
|
||
|
||
if (instances.size() % 2) {
|
||
// "odd number of oxyd stones";
|
||
// TODO
|
||
}
|
||
}
|
||
return Value();
|
||
}
|
||
|
||
void OxydStone::shuffle_colors()
|
||
{
|
||
vector<size_t> closed_oxyds;
|
||
size_t isize = instances.size();
|
||
for (size_t i=0; i<isize; ++i) {
|
||
if (instances[i]->state == CLOSED) {
|
||
closed_oxyds.push_back(i);
|
||
}
|
||
}
|
||
|
||
unsigned size = closed_oxyds.size();
|
||
if (size>1) {
|
||
for (unsigned i = 0; i<size; ++i) {
|
||
unsigned a = IntegerRand(0, static_cast<int> (size-2));
|
||
if (a >= i) ++a; // make a always different from j
|
||
|
||
OxydStone *o1 = instances[closed_oxyds[i]];
|
||
OxydStone *o2 = instances[closed_oxyds[a]];
|
||
|
||
string icolor, acolor;
|
||
o1->string_attrib("color", &icolor);
|
||
o2->string_attrib("color", &acolor);
|
||
|
||
o1->set_attrib("color", acolor.c_str());
|
||
o2->set_attrib("color", icolor.c_str());
|
||
}
|
||
}
|
||
}
|
||
|
||
void OxydStone::change_state(State newstate)
|
||
{
|
||
string flavor = "a";
|
||
string color = "1";
|
||
string_attrib("flavor", &flavor);
|
||
string_attrib("color", &color);
|
||
|
||
string modelname = string("st-oxyd") + flavor + color;
|
||
|
||
State oldstate = state;
|
||
state = newstate;
|
||
|
||
switch (newstate) {
|
||
case CLOSED:
|
||
set_model(string("st-oxyd")+flavor);
|
||
break;
|
||
|
||
case BLINKING:
|
||
set_model(modelname + "-blink");
|
||
break;
|
||
|
||
case OPEN:
|
||
if (oldstate == CLOSED) {
|
||
sound_event("oxydopen");
|
||
sound_event("oxydopened");
|
||
set_anim(modelname+"-opening");
|
||
} else {
|
||
set_model(modelname + "-open");
|
||
}
|
||
/* If this was the last closed oxyd stone, finish the
|
||
level */
|
||
if (find_if(instances.begin(),instances.end(),not_open)
|
||
==instances.end())
|
||
{
|
||
server::FinishLevel();
|
||
}
|
||
break;
|
||
|
||
case OPENING:
|
||
sound_event("oxydopen");
|
||
if (oldstate == CLOSED)
|
||
set_anim(modelname + "-opening");
|
||
else if (oldstate == CLOSING)
|
||
get_model()->reverse();
|
||
|
||
break;
|
||
|
||
case CLOSING:
|
||
if (oldstate == CLOSED || oldstate==CLOSING) {
|
||
state = oldstate;
|
||
return;
|
||
}
|
||
|
||
sound_event("oxydclose");
|
||
if (oldstate == OPENING)
|
||
get_model()->reverse();
|
||
else if (oldstate == BLINKING || oldstate == OPEN) {
|
||
set_anim(modelname + "-closing");
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
void OxydStone::animcb() {
|
||
if (state == CLOSING)
|
||
change_state(CLOSED);
|
||
else if (state == OPENING)
|
||
change_state(BLINKING);
|
||
else if (state == OPEN)
|
||
change_state(OPEN); // set the right model
|
||
}
|
||
|
||
void OxydStone::maybe_open_stone() {
|
||
if (state == CLOSED || state == CLOSING) {
|
||
int mycolor = int_attrib("color");
|
||
|
||
// Is another oxyd stone currently blinking?
|
||
InstanceList::iterator i;
|
||
i=find_if(instances.begin(), instances.end(), blinking_or_opening);
|
||
|
||
if (i != instances.end()) {
|
||
|
||
bool can_open;
|
||
|
||
if (server::GameCompatibility != GAMET_ENIGMA) {
|
||
// If colors match and stone (*i) is already blinking,
|
||
// open both stones. Close one of them otherwise.
|
||
// (This is the Oxyd behaviour; it doesn't work with
|
||
// some Enigma levels.)
|
||
can_open = (mycolor == (*i)->int_attrib("color") && (*i)->state==BLINKING);
|
||
}
|
||
else
|
||
can_open = (mycolor == (*i)->int_attrib("color"));
|
||
|
||
if (can_open) {
|
||
change_state(OPEN);
|
||
(*i)->change_state(OPEN);
|
||
} else {
|
||
(*i)->change_state(CLOSING);
|
||
change_state(OPENING);
|
||
}
|
||
}
|
||
else {
|
||
// no blinking stone? -> make this one blink
|
||
change_state(OPENING);
|
||
}
|
||
}
|
||
}
|
||
|
||
void OxydStone::actor_hit(const StoneContact &/*sc*/) {
|
||
maybe_open_stone();
|
||
}
|
||
|
||
void OxydStone::on_creation (GridPos)
|
||
{
|
||
string flavor = "a";
|
||
string_attrib("flavor", &flavor);
|
||
set_model(string("st-oxyd") + flavor);
|
||
photo_activate();
|
||
}
|
||
|
||
bool OxydStone::is_removable() const {
|
||
const Value* isStatic = get_attrib("static");
|
||
if (isStatic != NULL)
|
||
return !to_bool(*isStatic);
|
||
else
|
||
return true;
|
||
}
|
||
|
||
void OxydStone::on_removal(GridPos p)
|
||
{
|
||
photo_deactivate();
|
||
kill_model (p);
|
||
}
|
||
|
||
|
||
/* -------------------- Turnstiles -------------------- */
|
||
namespace
|
||
{
|
||
class Turnstile_Arm;
|
||
|
||
/*
|
||
** The stone at the center of a turnstile
|
||
*/
|
||
class Turnstile_Pivot_Base : public Stone {
|
||
public:
|
||
Turnstile_Pivot_Base(const char *kind);
|
||
|
||
protected:
|
||
bool rotate(bool clockwise, Object *impulse_sender);
|
||
|
||
friend class Turnstile_Arm; // uses rotate
|
||
|
||
private:
|
||
// Object interface
|
||
virtual Value on_message (const Message &m);
|
||
virtual void animcb();
|
||
|
||
// Private methods
|
||
DirectionBits arms_present() const;
|
||
bool no_stone (int xoff, int yoff) const;
|
||
void set_arm (Direction dir, RBI_vector &rubs);
|
||
void remove_arms (DirectionBits arms);
|
||
void rotate_arms (DirectionBits arms, bool clockwise);
|
||
void handleActorsAndItems(bool clockwise, Object *impulse_sender);
|
||
|
||
// Turnstile_Pivot_Base interface
|
||
virtual const char *model() const = 0;
|
||
virtual const char *anim() const = 0;
|
||
virtual bool oxyd_compatible() const = 0;
|
||
|
||
// Variables
|
||
bool active;
|
||
};
|
||
|
||
class Turnstile_Pivot : public Turnstile_Pivot_Base {
|
||
CLONEOBJ(Turnstile_Pivot);
|
||
public:
|
||
Turnstile_Pivot() : Turnstile_Pivot_Base(model()) {}
|
||
|
||
const char *model() const { return "st-turnstile"; }
|
||
const char *anim() const { return "st-turnstile-anim"; }
|
||
bool oxyd_compatible() const { return true; }
|
||
};
|
||
|
||
class Turnstile_Pivot_Green : public Turnstile_Pivot_Base {
|
||
CLONEOBJ(Turnstile_Pivot_Green);
|
||
public:
|
||
Turnstile_Pivot_Green() : Turnstile_Pivot_Base(model()) {}
|
||
|
||
const char *model() const { return "st-turnstile-green"; }
|
||
const char *anim() const { return "st-turnstile-green-anim"; }
|
||
bool oxyd_compatible() const { return false; }
|
||
};
|
||
|
||
/*
|
||
** The base class for any of the four arms of the turnstile
|
||
*/
|
||
class Turnstile_Arm : public Stone {
|
||
virtual Direction get_dir() const = 0;
|
||
|
||
void actor_hit(const StoneContact &sc);
|
||
void on_impulse(const Impulse& impulse);
|
||
|
||
Turnstile_Pivot_Base *get_pivot() {
|
||
Stone *st = GetStone (move (get_pos(), reverse(get_dir())));
|
||
return dynamic_cast<Turnstile_Pivot_Base*>(st);
|
||
}
|
||
|
||
bool is_movable () const { return true; }
|
||
protected:
|
||
Turnstile_Arm (const char *kind) : Stone(kind)
|
||
{}
|
||
};
|
||
|
||
class Turnstile_N : public Turnstile_Arm {
|
||
CLONEOBJ(Turnstile_N);
|
||
public:
|
||
Turnstile_N(): Turnstile_Arm("st-turnstile-n") {}
|
||
Direction get_dir () const { return NORTH; }
|
||
};
|
||
|
||
class Turnstile_S : public Turnstile_Arm {
|
||
CLONEOBJ(Turnstile_S);
|
||
Direction get_dir () const { return SOUTH; }
|
||
public:
|
||
Turnstile_S(): Turnstile_Arm("st-turnstile-s") {}
|
||
};
|
||
|
||
class Turnstile_E : public Turnstile_Arm {
|
||
CLONEOBJ(Turnstile_E);
|
||
Direction get_dir () const { return EAST; }
|
||
public:
|
||
Turnstile_E(): Turnstile_Arm("st-turnstile-e") {}
|
||
};
|
||
|
||
class Turnstile_W : public Turnstile_Arm {
|
||
CLONEOBJ(Turnstile_W);
|
||
Direction get_dir () const { return WEST; }
|
||
public:
|
||
Turnstile_W(): Turnstile_Arm("st-turnstile-w") {}
|
||
};
|
||
}
|
||
|
||
|
||
/* -------------------- Turnstile_Arm -------------------- */
|
||
|
||
void Turnstile_Arm::on_impulse(const Impulse& impulse) {
|
||
enum Action { ROTL, ROTR, stay };
|
||
static Action actions[4][4] = {
|
||
{ stay, ROTL, stay, ROTR }, // west arm
|
||
{ ROTR, stay, ROTL, stay }, // south arm
|
||
{ stay, ROTR, stay, ROTL }, // east arm
|
||
{ ROTL, stay, ROTR, stay } // north arm
|
||
};
|
||
|
||
Turnstile_Pivot_Base *pivot = get_pivot();
|
||
|
||
if (pivot) {
|
||
Action a = actions[get_dir()][impulse.dir];
|
||
if (a != stay) {
|
||
pivot->rotate(a == ROTR, impulse.sender); // ROTR is clockwise
|
||
}
|
||
}
|
||
else {
|
||
// Move arms not attached to a pivot individually
|
||
move_stone(impulse.dir);
|
||
}
|
||
}
|
||
|
||
void Turnstile_Arm::actor_hit(const StoneContact &sc)
|
||
{
|
||
maybe_push_stone(sc);
|
||
}
|
||
|
||
// --------------------------------------------
|
||
// Turnstile_Pivot_Base implementation
|
||
// --------------------------------------------
|
||
|
||
Turnstile_Pivot_Base::Turnstile_Pivot_Base(const char *kind)
|
||
: Stone (kind),
|
||
active (false)
|
||
{}
|
||
|
||
void Turnstile_Pivot_Base::animcb()
|
||
{
|
||
set_model(model());
|
||
active = false;
|
||
}
|
||
|
||
Value Turnstile_Pivot_Base::on_message (const Message &m)
|
||
{
|
||
if (m.message == "signal") {
|
||
int val = to_int (m.value);
|
||
if (val == 1)
|
||
rotate(false, 0);
|
||
else
|
||
rotate(true, 0);
|
||
}
|
||
return Value();
|
||
}
|
||
|
||
|
||
DirectionBits
|
||
Turnstile_Pivot_Base::arms_present() const
|
||
{
|
||
DirectionBits arms = NODIRBIT;
|
||
GridPos p = get_pos();
|
||
if (dynamic_cast<Turnstile_N*>(GetStone(move(p, NORTH))))
|
||
ecl::set_flags (arms, NORTHBIT);
|
||
if (dynamic_cast<Turnstile_S*>(GetStone(move(p, SOUTH))))
|
||
ecl::set_flags (arms, SOUTHBIT);
|
||
if (dynamic_cast<Turnstile_E*>(GetStone(move(p, EAST))))
|
||
ecl::set_flags (arms, EASTBIT);
|
||
if (dynamic_cast<Turnstile_W*>(GetStone(move(p, WEST))))
|
||
ecl::set_flags (arms, WESTBIT);
|
||
return arms;
|
||
}
|
||
|
||
bool Turnstile_Pivot_Base::no_stone (int xoff, int yoff) const {
|
||
GridPos p = get_pos();
|
||
p.x += xoff;
|
||
p.y += yoff;
|
||
return (0 == GetStone(p));
|
||
}
|
||
|
||
void Turnstile_Pivot_Base::remove_arms (DirectionBits arms) {
|
||
GridPos p = get_pos();
|
||
if (arms & NORTHBIT) KillStone (move (p, NORTH));
|
||
if (arms & EASTBIT) KillStone (move (p, EAST));
|
||
if (arms & SOUTHBIT) KillStone (move (p, SOUTH));
|
||
if (arms & WESTBIT) KillStone (move (p, WEST));
|
||
}
|
||
|
||
void Turnstile_Pivot_Base::rotate_arms (DirectionBits arms, bool clockwise) {
|
||
GridPos p = get_pos();
|
||
|
||
RBI_vector Nrubs;
|
||
RBI_vector Erubs;
|
||
RBI_vector Srubs;
|
||
RBI_vector Wrubs;
|
||
|
||
if (arms & NORTHBIT) GiveRubberBands(GetStone(move (p, NORTH)), Nrubs);
|
||
if (arms & EASTBIT) GiveRubberBands(GetStone(move (p, EAST)), Erubs);
|
||
if (arms & SOUTHBIT) GiveRubberBands(GetStone(move (p, SOUTH)), Srubs);
|
||
if (arms & WESTBIT) GiveRubberBands(GetStone(move (p, WEST)), Wrubs);
|
||
|
||
remove_arms(arms);
|
||
|
||
if (clockwise) {
|
||
if (arms & NORTHBIT) set_arm(EAST, Nrubs);
|
||
if (arms & EASTBIT) set_arm(SOUTH, Erubs);
|
||
if (arms & SOUTHBIT) set_arm(WEST, Srubs);
|
||
if (arms & WESTBIT) set_arm(NORTH, Wrubs);
|
||
}
|
||
else {
|
||
if (arms & NORTHBIT) set_arm(WEST, Nrubs);
|
||
if (arms & EASTBIT) set_arm(NORTH, Erubs);
|
||
if (arms & SOUTHBIT) set_arm(EAST, Srubs);
|
||
if (arms & WESTBIT) set_arm(SOUTH, Wrubs);
|
||
}
|
||
}
|
||
|
||
void Turnstile_Pivot_Base::set_arm (Direction dir, RBI_vector &rubs) {
|
||
const char *names[4] = { "st-turnstile-w", "st-turnstile-s",
|
||
"st-turnstile-e", "st-turnstile-n" };
|
||
Stone *st = MakeStone(names[dir]);
|
||
GridPos newp = move(get_pos(), dir);
|
||
SetStone (newp, st);
|
||
|
||
if (Item *it = GetItem(newp))
|
||
it->on_stonehit(st);
|
||
|
||
if (!rubs.empty())
|
||
for (RBI_vector::iterator i = rubs.begin(); i != rubs.end(); ++i)
|
||
AddRubberBand (i->act, st, i->data);
|
||
}
|
||
|
||
bool Turnstile_Pivot_Base::rotate(bool clockwise, Object *impulse_sender) {
|
||
if (active)
|
||
return false;
|
||
|
||
DirectionBits arms = arms_present();
|
||
bool can_rotate = true;
|
||
|
||
if (clockwise) {
|
||
if (arms & NORTHBIT) {
|
||
can_rotate &= no_stone(+1,-1);
|
||
if (! (arms & EASTBIT)) can_rotate &= no_stone(+1,0);
|
||
}
|
||
if (arms & WESTBIT) {
|
||
can_rotate &= no_stone(-1,-1);
|
||
if (! (arms & NORTHBIT)) can_rotate &= no_stone(0,-1);
|
||
}
|
||
if (arms & SOUTHBIT) {
|
||
can_rotate &= no_stone(-1,+1);
|
||
if (! (arms & WESTBIT)) can_rotate &= no_stone(-1,0);
|
||
}
|
||
if (arms & EASTBIT) {
|
||
can_rotate &= no_stone(+1,+1);
|
||
if (! (arms & SOUTHBIT)) can_rotate &= no_stone(0,+1);
|
||
}
|
||
}
|
||
else {
|
||
if (arms & NORTHBIT) {
|
||
can_rotate &= no_stone(-1,-1);
|
||
if (! (arms & WESTBIT)) can_rotate &= no_stone(-1,0);
|
||
}
|
||
if (arms & WESTBIT) {
|
||
can_rotate &= no_stone(-1,+1);
|
||
if (! (arms & SOUTHBIT)) can_rotate &= no_stone(0,+1);
|
||
}
|
||
if (arms & SOUTHBIT) {
|
||
can_rotate &= no_stone(+1,+1);
|
||
if (! (arms & EASTBIT)) can_rotate &= no_stone(+1,0);
|
||
}
|
||
if (arms & EASTBIT) {
|
||
can_rotate &= no_stone(+1,-1);
|
||
if (! (arms & NORTHBIT)) can_rotate &= no_stone(0,-1);
|
||
}
|
||
}
|
||
|
||
if (can_rotate) {
|
||
sound_event (clockwise ? "turnstileright" : "turnstileleft");
|
||
sound_event("movesmall");
|
||
|
||
active = true;
|
||
set_anim(anim());
|
||
rotate_arms(arms, clockwise);
|
||
handleActorsAndItems(clockwise, impulse_sender);
|
||
|
||
PerformAction (this, clockwise == 0);
|
||
server::IncMoveCounter();
|
||
}
|
||
return can_rotate;
|
||
}
|
||
|
||
namespace {
|
||
bool calc_arm_seen (bool cw, DirectionBits arms, int field) {
|
||
// for each field calculate whether an arm has passed by, first
|
||
// counterclockwise and then clockwise:
|
||
const DirectionBits neededArm[2][8] = {
|
||
{WESTBIT, NORTHBIT, NORTHBIT, EASTBIT, EASTBIT, SOUTHBIT, SOUTHBIT, WESTBIT},
|
||
{NORTHBIT, NORTHBIT, EASTBIT, EASTBIT, SOUTHBIT, SOUTHBIT, WESTBIT, WESTBIT}
|
||
};
|
||
return (arms & neededArm[cw][field]) != 0;
|
||
}
|
||
}
|
||
|
||
void Turnstile_Pivot_Base::handleActorsAndItems(bool clockwise, Object *impulse_sender) {
|
||
const int to_index[3][3] = { // (read this transposed)
|
||
{ 0, 7, 6 }, // x == 0
|
||
{ 1,-1, 5 }, // x == 1
|
||
{ 2, 3, 4 } // x == 2
|
||
};
|
||
const int to_x[8] = { -1, 0, 1, 1, 1, 0, -1, -1 };
|
||
const int to_y[8] = { -1, -1, -1, 0, 1, 1, 1, 0 };
|
||
|
||
bool arm_seen[8];
|
||
DirectionBits arms = arms_present(); // Note: already the rotated state
|
||
for (int i = 0; i<8; ++i)
|
||
arm_seen[i] = calc_arm_seen (clockwise, arms, i);
|
||
|
||
// ---------- Handle items in range ----------
|
||
GridPos pv_pos = get_pos();
|
||
for (int i = 0; i<8; ++i)
|
||
if (arm_seen[i]) {
|
||
GridPos item_pos(pv_pos.x+to_x[i], pv_pos.y+to_y[i]);
|
||
if (Item *it = GetItem(item_pos))
|
||
it->on_stonehit(this); // hit with pivot (shouldn't matter)
|
||
}
|
||
|
||
// ---------- Handle actors in range ----------
|
||
vector<Actor*> actorsInRange;
|
||
|
||
// tested range is sqrt(sqr(1.5)+sqr(1.5))
|
||
if (!GetActorsInRange(pv_pos.center(), 2.124, actorsInRange))
|
||
return;
|
||
|
||
vector<Actor*>::iterator iter = actorsInRange.begin(), end = actorsInRange.end();
|
||
for (; iter != end; ++iter) {
|
||
Actor *ac = *iter;
|
||
const V2 &ac_center = ac->get_pos();
|
||
GridPos ac_pos(ac_center);
|
||
int dx = ac_pos.x-pv_pos.x;
|
||
int dy = ac_pos.y-pv_pos.y;
|
||
|
||
// ignore if actor is not inside the turnstile square or is not
|
||
// in distance of the the rotating arms
|
||
if ((dx<-1 || dx>1 || dy<-1 || dy>1) ||
|
||
(length(ac->get_pos() - pv_pos.center()) > 1.58114 + ac->get_actorinfo()->radius))
|
||
continue;
|
||
|
||
int idx_source = to_index[dx+1][dy+1];
|
||
if (idx_source == -1)
|
||
continue; // actor inside pivot -- should not happen
|
||
|
||
const int rot_index[4][8] = {
|
||
// The warp-destinations for actors. Why different destinations
|
||
// for oxyd/non-oxyd-type turnstiles? Imagine the actor on position
|
||
// 1 (North of pivot), the turnstile rotates anticlockwise. Then
|
||
// a green turnstile-arm, if at all, would push the actor one field
|
||
// to the left (position 0). Now assume it's a red turnstile. If the
|
||
// actor is to be warped, it has to be the one that activated the
|
||
// turnstile. Yet it is on position 1, in principle not able to
|
||
// hit an arm. But it can, if it hits fast enough on the edge of
|
||
// pivot and left arm. In this case, the actor should be handled
|
||
// as if on position 1, thus warping to 6.
|
||
{ 6, 0, 0, 2, 2, 4, 4, 6 }, // anticlockwise
|
||
{ 2, 2, 4, 4, 6, 6, 0, 0 }, // clockwise
|
||
{ 6, 6, 0, 0, 2, 2, 4, 4 }, // anticlockwise (oxyd-compatible)
|
||
{ 2, 4, 4, 6, 6, 0, 0, 2 }, // clockwise (oxyd-compatible)
|
||
};
|
||
|
||
bool compatible = oxyd_compatible();
|
||
int idx_target = rot_index[clockwise+2*compatible][idx_source]; // destination index
|
||
bool do_warp = false; // move the actor along with the turnstile?
|
||
|
||
if (compatible) {
|
||
// Move only the actor that hit the turnstile in Oxyd mode
|
||
do_warp = (ac == dynamic_cast<Actor*>(impulse_sender));
|
||
if (!do_warp && arm_seen[idx_source])
|
||
SendMessage(ac, "shatter"); // hit by an arm
|
||
} else { // green turnstile
|
||
// move all actors only if pushed by an arm
|
||
do_warp = arm_seen[idx_source];
|
||
}
|
||
|
||
if (!do_warp)
|
||
continue;
|
||
|
||
// Pushing an actor out of the level results in a shatter (no warp) instead
|
||
GridPos ac_target_pos(pv_pos.x+to_x[idx_target], pv_pos.y+to_y[idx_target]);
|
||
|
||
if(!IsInsideLevel(ac_target_pos)) {
|
||
SendMessage(ac, "shatter");
|
||
continue;
|
||
}
|
||
|
||
world::WarpActor(ac, ac_target_pos.x+.5, ac_target_pos.y+.5, false);
|
||
|
||
if (Stone *st = GetStone(ac_target_pos)) {
|
||
|
||
// destination is blocked
|
||
|
||
Turnstile_Arm *arm = dynamic_cast<Turnstile_Arm*>(st);
|
||
if (arm && !compatible) { // if blocking stone is turnstile arm -> hit it!
|
||
const int impulse_dir[2][8] = {
|
||
// anticlockwise
|
||
{ SOUTHBIT|WESTBIT, WESTBIT, NORTHBIT|WESTBIT, NORTHBIT,
|
||
NORTHBIT|EASTBIT, EASTBIT, SOUTHBIT|EASTBIT, SOUTHBIT },
|
||
// clockwise
|
||
{ NORTHBIT|EASTBIT, EASTBIT, SOUTHBIT|EASTBIT, SOUTHBIT,
|
||
SOUTHBIT|WESTBIT, WESTBIT, NORTHBIT|WESTBIT, NORTHBIT }
|
||
};
|
||
|
||
DirectionBits possible_impulses =
|
||
static_cast<DirectionBits>(impulse_dir[clockwise][idx_target]);
|
||
|
||
for (int d = 0; d<4; ++d)
|
||
if (has_dir(possible_impulses, Direction(d)))
|
||
ac->send_impulse(ac_target_pos, Direction(d));
|
||
|
||
// if (GetStone(ac_target_pos) == 0) // arm disappeared
|
||
// break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// @@@ FIXME: it's possible that two actors are moved to the same
|
||
// destination field. In that case the second actor is put on top of
|
||
// the first actor (happens only in non-oxyd-compat-mode with three
|
||
// balls or pullers/impulsestones)
|
||
//
|
||
// Note: With black and whiteball it's normally no problem, because
|
||
// when one of the actors was moving, it's looking natural. Problems
|
||
// occur when small balls come into play.
|
||
|
||
}
|
||
|
||
|
||
/* -------------------- Mail stone -------------------- */
|
||
|
||
namespace
|
||
{
|
||
class MailStone : public Stone {
|
||
CLONEOBJ(MailStone);
|
||
Direction m_dir;
|
||
|
||
|
||
MailStone (const char *kind, Direction dir);
|
||
void actor_hit (const StoneContact &sc);
|
||
|
||
GridPos find_pipe_endpoint();
|
||
public:
|
||
static void setup();
|
||
};
|
||
}
|
||
|
||
void MailStone::setup()
|
||
{
|
||
Register (new MailStone ("st-mail-n", NORTH));
|
||
Register (new MailStone ("st-mail-e", EAST));
|
||
Register (new MailStone ("st-mail-s", SOUTH));
|
||
Register (new MailStone ("st-mail-w", WEST));
|
||
}
|
||
|
||
MailStone::MailStone (const char *kind, Direction dir)
|
||
: Stone(kind), m_dir(dir)
|
||
{}
|
||
|
||
|
||
void MailStone::actor_hit (const StoneContact &sc)
|
||
{
|
||
if (enigma::Inventory *inv = player::GetInventory(sc.actor)) {
|
||
if (Item *it = inv->get_item(0)) {
|
||
GridPos p = find_pipe_endpoint();
|
||
if (world::IsInsideLevel(p) && it->can_drop_at (p)) {
|
||
it = inv->yield_first();
|
||
player::RedrawInventory (inv);
|
||
it->drop(sc.actor, p);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/** About recursion detection while finding the end of a mailpipe
|
||
*
|
||
* Since there are no possibilities for forking a mailpipe, there is only one
|
||
* cause that may lead to a closed, circular mailpipe. This is if a pipeitem is
|
||
* placed exactly under the mailstone. But not every pipepiece is dangerous,
|
||
* there are some that can be placed under the mailstone without problems.
|
||
*
|
||
* The mailpipe is only closed to a circular one if the pipepiece under the
|
||
* mailstone has the same 'output'-direction as the stone.
|
||
*/
|
||
GridPos MailStone::find_pipe_endpoint()
|
||
{
|
||
GridPos p = get_pos();
|
||
Direction move_dir = m_dir;
|
||
GridPos q = p; // Store the stonepos for recursion detection.
|
||
|
||
while (move_dir != NODIR) {
|
||
p.move (move_dir);
|
||
if (Item *it = world::GetItem(p)) {
|
||
switch (get_id(it)) {
|
||
case it_pipe_h:
|
||
if (!(move_dir == EAST || move_dir == WEST))
|
||
move_dir = NODIR;
|
||
break;
|
||
case it_pipe_v:
|
||
if (!(move_dir == SOUTH || move_dir == NORTH))
|
||
move_dir = NODIR;
|
||
break;
|
||
case it_pipe_ne:
|
||
if (move_dir == SOUTH) move_dir = EAST;
|
||
else if (move_dir == WEST) move_dir = NORTH;
|
||
else move_dir = NODIR;
|
||
break;
|
||
case it_pipe_es:
|
||
if (move_dir == NORTH) move_dir = EAST;
|
||
else if (move_dir == WEST) move_dir = SOUTH;
|
||
else move_dir = NODIR;
|
||
break;
|
||
case it_pipe_sw:
|
||
if (move_dir == NORTH) move_dir = WEST;
|
||
else if (move_dir == EAST) move_dir = SOUTH;
|
||
else move_dir = NODIR;
|
||
break;
|
||
case it_pipe_wn:
|
||
if (move_dir == SOUTH) move_dir = WEST;
|
||
else if (move_dir == EAST) move_dir = NORTH;
|
||
else move_dir = NODIR;
|
||
break;
|
||
default:
|
||
move_dir = NODIR;; // end of pipe reached
|
||
}
|
||
} else
|
||
move_dir = NODIR;
|
||
|
||
if (p == q)
|
||
ASSERT(move_dir != m_dir, XLevelRuntime, "Mailpipe is circular! Recursion detected!");
|
||
}
|
||
return p;
|
||
}
|
||
|
||
|
||
/* -------------------- Chess stone -------------------- */
|
||
|
||
namespace
|
||
{
|
||
class ChessStone : public Stone, public TimeHandler {
|
||
CLONEOBJ(ChessStone);
|
||
public:
|
||
ChessStone (int color) : Stone("st-chess") {
|
||
newcolor = color;
|
||
Stone::set_attrib("color", color);
|
||
destination = GridPos(0,0);
|
||
capture_retry = 0;
|
||
rememberFalling = false;
|
||
rememberSwamp = false;
|
||
state = IDLE;
|
||
}
|
||
virtual ~ChessStone() {
|
||
GameTimer.remove_alarm (this);
|
||
}
|
||
void init_model();
|
||
void animcb();
|
||
void set_attrib(const string& key, const Value &val);
|
||
virtual Value message(const string &msg, const Value &v);
|
||
void actor_hit(const StoneContact &sc);
|
||
void alarm();
|
||
private:
|
||
// Variables and Constants
|
||
enum State {IDLE, APPEARING, DISAPPEARING, CAPTURING,
|
||
CAPTURED, FALLING, SWAMP} state;
|
||
GridPos destination;
|
||
int newcolor; // Buffers a color-changing message while not IDLE.
|
||
int capture_retry;
|
||
static const int max_capture_retry = 20;
|
||
static double capture_interval;
|
||
static double hit_threshold;
|
||
bool rememberFalling;
|
||
bool rememberSwamp;
|
||
|
||
// Methods
|
||
string get_model_name();
|
||
Value maybe_move_to(Direction dir1, Direction dir2);
|
||
virtual Value message_move(Direction dir1, Direction dir2);
|
||
bool try_state(State newstate);
|
||
void set_color(int color);
|
||
void on_floor_change();
|
||
|
||
// Interface for st-swap and st-pull
|
||
bool is_removable() const { return state == IDLE; }
|
||
};
|
||
double ChessStone::capture_interval = 0.1;
|
||
double ChessStone::hit_threshold = 3.0;
|
||
|
||
string ChessStone::get_model_name() {
|
||
string mname = get_kind();
|
||
mname += int_attrib("color") == 0.0 ? "_black" : "_white";
|
||
return mname;
|
||
}
|
||
|
||
void ChessStone::init_model() { set_model(get_model_name()); }
|
||
|
||
void ChessStone::animcb() {
|
||
Stone *st;
|
||
switch (state) {
|
||
case APPEARING:
|
||
if(try_state(IDLE))
|
||
// Maybe falling in the meantime? Otherwise:
|
||
init_model();
|
||
break;
|
||
case DISAPPEARING:
|
||
// Maybe the floor has changed into swamp or abyss?
|
||
if(!rememberFalling && !rememberSwamp) {
|
||
if(try_state(APPEARING)) {
|
||
st = GetStone(destination);
|
||
if(st) {
|
||
// Something went wrong while killing the old
|
||
// stone, or maybe a third one intervened.
|
||
// Don't move, just reappear at old position.
|
||
} else {
|
||
move_stone(destination, "movesmall");
|
||
SendMessage(GetFloor(destination), "capture");
|
||
}
|
||
// maybe a floor-change has happened, but during
|
||
// state APPEARING this doesn't mean anything:
|
||
set_anim(get_model_name()+"-appearing");
|
||
}
|
||
break;
|
||
}
|
||
// Else: If floor is swamp or abyss, kill the stone. Do this
|
||
// just by continuing to the next case:
|
||
case CAPTURED:
|
||
case FALLING:
|
||
case SWAMP:
|
||
KillStone(get_pos());
|
||
break;
|
||
default:
|
||
ASSERT(0, XLevelRuntime, "ChessStone: inconsistent state in animcb()");
|
||
}
|
||
}
|
||
|
||
void ChessStone::actor_hit(const StoneContact &sc) {
|
||
if (player::WieldedItemIs (sc.actor, "it-magicwand")) {
|
||
sound_event ("stonepaint");
|
||
set_color(1 - int_attrib("color"));
|
||
// If not IDLE, color will be set next time IDLE is set.
|
||
} else if ((sc.actor->get_attrib("blackball") && int_attrib("color") == 0)
|
||
|| (sc.actor->get_attrib("whiteball") && int_attrib("color") == 1)) {
|
||
V2 v = sc.actor->get_vel();
|
||
Direction dir1 = get_push_direction(sc);
|
||
if(dir1 == NODIR) return;
|
||
Direction dir2 = NODIR;
|
||
if (dir1 == SOUTH || dir1 == NORTH) {
|
||
dir2 = v[0] > hit_threshold ? EAST :
|
||
v[0] < -hit_threshold ? WEST : NODIR;
|
||
} else {
|
||
dir2 = v[1] > hit_threshold ? SOUTH :
|
||
v[1] < -hit_threshold ? NORTH : NODIR;
|
||
}
|
||
if(dir2 == NODIR) return;
|
||
// maybe_move_to tests for state == IDLE by itself.
|
||
maybe_move_to(dir1, dir2);
|
||
}
|
||
}
|
||
|
||
Value ChessStone::maybe_move_to(Direction dir1, Direction dir2) {
|
||
if(state == IDLE) {
|
||
// check for fire, step by step
|
||
destination = move(get_pos(), dir1);
|
||
if(Item *it = GetItem(destination))
|
||
if(get_id(it) == it_burnable_burning)
|
||
return Value();
|
||
destination = move(destination, dir2);
|
||
if(Item *it = GetItem(destination))
|
||
if(get_id(it) == it_burnable_burning)
|
||
return Value();
|
||
destination = move(destination, dir1);
|
||
if(Item *it = GetItem(destination))
|
||
if(get_id(it) == it_burnable_burning)
|
||
return Value();
|
||
// check for boundary
|
||
if(!IsInsideLevel(destination)) return Value();
|
||
// check for stone
|
||
if(!GetStone(destination)) {
|
||
// Simple case: Just move.
|
||
if(try_state(DISAPPEARING)) {
|
||
set_anim(get_model_name() + "-disappearing");
|
||
return Value(1);
|
||
} else
|
||
return Value();
|
||
} else {
|
||
// Test stone. Is it opposite chess stone or totally another one?
|
||
Stone *st = GetStone(destination);
|
||
const Value *col = get_attrib("color");
|
||
if(to_int(SendMessage(st, "capture", Value(get_model_name()))) ) {
|
||
// Give it some time for animation, then replace it.
|
||
ASSERT(try_state(CAPTURING), XLevelRuntime,
|
||
"ChessStone: strange things happening in maybe_move_to");
|
||
// must work, because state is IDLE
|
||
GameTimer.set_alarm(this, capture_interval, false);
|
||
capture_retry = 0;
|
||
return Value(1);
|
||
}
|
||
return Value();
|
||
}
|
||
}
|
||
return Value();
|
||
}
|
||
|
||
void ChessStone::alarm() {
|
||
switch(state) {
|
||
case CAPTURING:
|
||
if(!GetStone(destination)) {
|
||
if(try_state(DISAPPEARING))
|
||
set_anim(get_model_name() + "-disappearing");
|
||
break;
|
||
} else if(capture_retry < max_capture_retry) {
|
||
++capture_retry;
|
||
GameTimer.set_alarm(this, capture_interval, false);
|
||
} else {
|
||
// Cancel efforts to capture foreign stone.
|
||
capture_retry = 0;
|
||
try_state(IDLE);
|
||
}
|
||
break;
|
||
default:
|
||
ASSERT(0, XLevelRuntime, "ChessStone: inconsistent state in alarm()");
|
||
}
|
||
}
|
||
|
||
Value ChessStone::message(const string &msg, const Value &v) {
|
||
if(msg == "capture") {
|
||
if(state == IDLE && to_string(v) != get_model_name())
|
||
if(try_state(CAPTURED)) {
|
||
set_anim(get_model_name() + "-captured");
|
||
return Value(1);
|
||
}
|
||
return Value();
|
||
} else if(msg == "move_nne") { return message_move(NORTH, EAST); }
|
||
else if(msg == "move_een") { return message_move(EAST, NORTH); }
|
||
else if(msg == "move_ees") { return message_move(EAST, SOUTH); }
|
||
else if(msg == "move_sse") { return message_move(SOUTH, EAST); }
|
||
else if(msg == "move_ssw") { return message_move(SOUTH, WEST); }
|
||
else if(msg == "move_wws") { return message_move(WEST, SOUTH); }
|
||
else if(msg == "move_wwn") { return message_move(WEST, NORTH); }
|
||
else if(msg == "move_nnw") { return message_move(NORTH, WEST); }
|
||
else if(msg == "move") {
|
||
Direction dir1 = (Direction) int_attrib("direction1");
|
||
Direction dir2 = (Direction) int_attrib("direction2");
|
||
return message_move(dir1, dir2);
|
||
} else if(msg == "signal") { set_color(to_int(v)); }
|
||
else if(msg == "flip") { set_color(1 - int_attrib("color")); }
|
||
else
|
||
return Stone::message(msg, v);
|
||
return Value();
|
||
}
|
||
|
||
Value ChessStone::message_move(Direction dir1, Direction dir2) {
|
||
// Restrict message-moves to chess-knight-moves
|
||
// state == IDLE is tested by maybe_move_to.
|
||
if((((dir1==NORTH || dir1==SOUTH) && (dir2==EAST || dir2==WEST))
|
||
|| ((dir1==EAST || dir1==WEST) && (dir2==SOUTH || dir2==NORTH))))
|
||
return maybe_move_to(dir1, dir2);
|
||
return Value();
|
||
}
|
||
|
||
bool ChessStone::try_state(State newstate) {
|
||
if (state != FALLING && state != SWAMP) {
|
||
// Switch to FALLING or SWAMP only when IDLE,
|
||
// but remember them!
|
||
if (newstate == FALLING)
|
||
rememberFalling = true;
|
||
else if (newstate == SWAMP)
|
||
rememberSwamp = true;
|
||
else
|
||
state = newstate;
|
||
if(state == IDLE && newcolor != int_attrib("color"))
|
||
set_color(newcolor);
|
||
if(state == IDLE && rememberFalling) {
|
||
state = FALLING;
|
||
set_anim(get_model_name() + "-disappearing");
|
||
}
|
||
if(state == IDLE && rememberSwamp) {
|
||
state = SWAMP;
|
||
set_anim(get_model_name() + "-swamp");
|
||
}
|
||
}
|
||
return state == newstate;
|
||
}
|
||
|
||
void ChessStone::set_attrib(const string& key, const Value &val) {
|
||
if(key == "color") {
|
||
set_color(to_int(val));
|
||
} else
|
||
Stone::set_attrib(key, val);
|
||
}
|
||
|
||
void ChessStone::set_color(int color) {
|
||
if(color != 0 && color != 1) {
|
||
ASSERT(0, XLevelRuntime, "ChessStone: argument to color not 0 or 1");
|
||
}
|
||
if(state == IDLE) {
|
||
Stone::set_attrib("color", color);
|
||
newcolor = color;
|
||
init_model();
|
||
} else {
|
||
// Remember this color and set it the next time IDLE is set.
|
||
newcolor = color;
|
||
}
|
||
}
|
||
|
||
void ChessStone::on_floor_change() {
|
||
Floor *fl = GetFloor(get_pos());
|
||
if (fl != NULL) {
|
||
if (fl->is_kind("fl-abyss"))
|
||
try_state(FALLING);
|
||
if (fl->is_kind("fl-swamp"))
|
||
try_state(SWAMP);
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
/* -------------------- Light Passenger Stone -------------------- */
|
||
namespace
|
||
{
|
||
class LightPassengerStone : public PhotoStone, public TimeHandler {
|
||
CLONEOBJ(LightPassengerStone);
|
||
DECL_TRAITS;
|
||
public:
|
||
LightPassengerStone(bool active_) : PhotoStone("st-lightpassenger"),
|
||
skateDir (NODIR), state(active_ ? ACTIVE : INACTIVE),
|
||
isLighted (false) { }
|
||
|
||
virtual ~LightPassengerStone() {
|
||
GameTimer.remove_alarm (this);
|
||
}
|
||
|
||
private:
|
||
Direction skateDir;
|
||
//bool isActive;
|
||
bool isLighted;
|
||
enum State {INACTIVE, ACTIVE, BLINK, BREAK} state;
|
||
//bool isBlinking;
|
||
|
||
void on_creation(GridPos p) {
|
||
PhotoStone::on_creation(p);
|
||
photo_activate();
|
||
}
|
||
void on_removal(GridPos p) {
|
||
photo_deactivate();
|
||
PhotoStone::on_removal(p);
|
||
}
|
||
void init_model() {
|
||
switch(state) {
|
||
case INACTIVE:
|
||
set_anim("st-lightpassenger_off"); break;
|
||
case ACTIVE:
|
||
set_anim("st-lightpassenger"); break;
|
||
case BLINK:
|
||
set_anim("st-lightpassenger-blink"); break;
|
||
case BREAK:
|
||
GridPos p = get_pos();
|
||
bool NorthSouth = lasers::LightFrom(p,NORTH) &&
|
||
lasers::LightFrom(p,SOUTH);
|
||
bool EastWest = lasers::LightFrom(p,EAST) &&
|
||
lasers::LightFrom(p,WEST);
|
||
sound_event ("stonedestroy");
|
||
if(NorthSouth && !EastWest)
|
||
set_anim("st-lightpassenger-break-v");
|
||
else if(!NorthSouth && EastWest)
|
||
set_anim("st-lightpassenger-break-h");
|
||
else // and this even in case the laser recently disappeared
|
||
set_anim("st-lightpassenger-break-hv");
|
||
break;
|
||
}
|
||
}
|
||
void animcb() {
|
||
ASSERT(state == BREAK, XLevelRuntime,
|
||
"LightPassengerStone: animcb called with inconsistent state");
|
||
KillStone(get_pos());
|
||
}
|
||
void notify_laseroff() {
|
||
if (isLighted) GameTimer.remove_alarm(this);
|
||
isLighted = false;
|
||
if(state == BLINK) {
|
||
state = ACTIVE;
|
||
init_model();
|
||
}
|
||
}
|
||
void notify_laseron() {
|
||
if (!isLighted) GameTimer.set_alarm(this, get_interval(), false);
|
||
isLighted = true;
|
||
}
|
||
|
||
double get_interval() {
|
||
/* Interval is calculated from
|
||
1 + friction_factor * friction
|
||
interval = -------------------------------- * baseinterval
|
||
1 + gradient_factor * gradient
|
||
and min-maxed to sensible values. "gradient" is just
|
||
the force resulting from floor->add_force. "baseinterval"
|
||
is 50 ms or the interval given in "interval".
|
||
*/
|
||
double base = 0.05;
|
||
if (const Value *v = this->get_attrib("interval"))
|
||
base = to_double(*v);
|
||
if (const Value *f = this->get_attrib("friction_factor"))
|
||
base *= 1.0 + to_double(*f) * GetFloor(get_pos())->friction();
|
||
if (const Value *g = this->get_attrib("gradient_factor"))
|
||
if (skateDir != NODIR) {
|
||
V2 vec = V2(0.0,0.0);
|
||
double quot = 0;
|
||
GetFloor(get_pos())->add_force(0, vec);
|
||
quot = skateDir == NORTH ? -vec[1] : skateDir == SOUTH ? vec[1] :
|
||
skateDir == EAST ? vec[0] : skateDir == WEST ? -vec[0] : 0;
|
||
base /= max(1.0 + to_double(*g) * quot, 0.01);
|
||
}
|
||
return max(base, 0.02);
|
||
}
|
||
|
||
void set_on(bool newon){
|
||
if(state == INACTIVE && newon) {
|
||
state = ACTIVE;
|
||
init_model();
|
||
if(isLighted)
|
||
GameTimer.set_alarm(this, get_interval(), false);
|
||
} else if((state == ACTIVE || state == BLINK) && !newon) {
|
||
state = INACTIVE;
|
||
init_model();
|
||
skateDir = NODIR;
|
||
}
|
||
}
|
||
|
||
virtual Value message (const string &msg, const Value &v) {
|
||
if (msg == "onoff") set_on(state == INACTIVE);
|
||
else if (msg == "signal") set_on(to_int(v) != 0);
|
||
else if (msg == "on") set_on(true);
|
||
else if (msg == "off") set_on(false);
|
||
else if (msg == "trigger") set_on(state == INACTIVE);
|
||
return Value();
|
||
}
|
||
|
||
void alarm() {
|
||
if(isLighted && (state == ACTIVE || state == BLINK)) {
|
||
GridPos p = get_pos();
|
||
int toSouth = (lasers::LightFrom(p,NORTH)?1:0)
|
||
+ (lasers::LightFrom(p,SOUTH)?-1:0);
|
||
int toWest = (lasers::LightFrom(p,EAST)?1:0)
|
||
+ (lasers::LightFrom(p,WEST)?-1:0);
|
||
if(toSouth * toWest != 0) {
|
||
// Light is coming from two directions. Choose the one you are
|
||
// *not* coming from (thus changing beams), in doubt: random.
|
||
if(skateDir == NORTH || skateDir == SOUTH)
|
||
toSouth = 0;
|
||
if(skateDir == EAST || skateDir == WEST)
|
||
toWest = 0;
|
||
if(skateDir == NODIR) {
|
||
toSouth = IntegerRand(0,1) ? 0 : toSouth;
|
||
toWest = toSouth ? 0 : toWest;
|
||
}
|
||
}
|
||
skateDir = (toSouth == 1) ? SOUTH : (toSouth == -1) ? NORTH :
|
||
(toWest == 1) ? WEST : (toWest == -1) ? EAST : NODIR;
|
||
if(skateDir == NODIR && state != BLINK) {
|
||
// No direction but lighted? Seems to be lasers from
|
||
// two opposite directions. Be sure and then start blinking.
|
||
if(lasers::LightFrom(p,EAST) || lasers::LightFrom(p,WEST) ||
|
||
lasers::LightFrom(p,NORTH) || lasers::LightFrom(p,SOUTH)) {
|
||
state = BLINK;
|
||
init_model();
|
||
}
|
||
} else if(skateDir != NODIR) {
|
||
if(state == BLINK) {
|
||
state == ACTIVE;
|
||
init_model();
|
||
}
|
||
if(GetStone(move(p, skateDir))) {
|
||
// Skipping each second turn makes the passenger stone seem
|
||
// slower when pushing another stone. This looks more
|
||
// natural. That's why impulse is delayed:
|
||
send_impulse(move(p, skateDir), skateDir, get_interval());
|
||
}
|
||
move_stone(skateDir);
|
||
}
|
||
GameTimer.set_alarm(this, get_interval(), false);
|
||
}
|
||
}
|
||
|
||
void on_impulse(const Impulse &impulse) {
|
||
Actor *a = dynamic_cast<Actor*>(impulse.sender);
|
||
if(!a && (!isLighted || state == INACTIVE))
|
||
move_stone(impulse.dir);
|
||
}
|
||
void actor_hit(const StoneContact &sc) {
|
||
Actor *a = sc.actor;
|
||
if(a && state == BLINK && player::WieldedItemIs(a, "it-hammer")) {
|
||
state = BREAK;
|
||
init_model();
|
||
}
|
||
}
|
||
bool is_movable () const { return false; }
|
||
};
|
||
|
||
DEF_TRAITS(LightPassengerStone,"st-lightpassenger", st_lightpassenger);
|
||
}
|
||
|
||
// --------------------------------------------------------------------------------
|
||
|
||
void stones::Init_complex()
|
||
{
|
||
Register(new BolderStone);
|
||
Register("st-bolder-n", new BolderStone(NORTH));
|
||
Register("st-bolder-e", new BolderStone(EAST));
|
||
Register("st-bolder-s", new BolderStone(SOUTH));
|
||
Register("st-bolder-w", new BolderStone(WEST));
|
||
|
||
Register(new BlockerStone(true));
|
||
Register(new BlockerStone(false));
|
||
|
||
Register(new Door);
|
||
Register("st-door-h", new Door("h"));
|
||
Register("st-door-v", new Door("v"));
|
||
Register("st-door-h-open", new Door("h", true));
|
||
Register("st-door-v-open", new Door("v", true));
|
||
Register(new Door_a);
|
||
Register(new Door_b);
|
||
Register(new Door_c);
|
||
|
||
Register(new HollowStoneImpulseStone);
|
||
|
||
MailStone::setup();
|
||
|
||
Register(new MovableImpulseStone);
|
||
|
||
Register(new OneWayStone);
|
||
Register("st-oneway-n", new OneWayStone(NORTH));
|
||
Register("st-oneway-e", new OneWayStone(EAST));
|
||
Register("st-oneway-s", new OneWayStone(SOUTH));
|
||
Register("st-oneway-w", new OneWayStone(WEST));
|
||
Register(new OneWayStone_black);
|
||
Register("st-oneway_black-n", new OneWayStone_black(NORTH));
|
||
Register("st-oneway_black-e", new OneWayStone_black(EAST));
|
||
Register("st-oneway_black-s", new OneWayStone_black(SOUTH));
|
||
Register("st-oneway_black-w", new OneWayStone_black(WEST));
|
||
Register(new OneWayStone_white);
|
||
Register("st-oneway_white-n", new OneWayStone_white(NORTH));
|
||
Register("st-oneway_white-e", new OneWayStone_white(EAST));
|
||
Register("st-oneway_white-s", new OneWayStone_white(SOUTH));
|
||
Register("st-oneway_white-w", new OneWayStone_white(WEST));
|
||
|
||
Register(new OxydStone);
|
||
|
||
Register (new PullStone);
|
||
|
||
Register( new VolcanoStone);
|
||
Register("st-volcano_inactive", new VolcanoStone(VolcanoStone::INACTIVE));
|
||
Register("st-volcano_active", new VolcanoStone(VolcanoStone::ACTIVE));
|
||
|
||
// PerOxyd/Enigma compatible puzzle stones:
|
||
Register(new PuzzleStone(0, false));
|
||
Register("st-puzzle-hollow", new PuzzleStone(1, false));
|
||
Register("st-puzzle-n", new PuzzleStone(9, false));
|
||
Register("st-puzzle-e", new PuzzleStone(5, false));
|
||
Register("st-puzzle-s", new PuzzleStone(3, false));
|
||
Register("st-puzzle-w", new PuzzleStone(2, false));
|
||
Register("st-puzzle-ne", new PuzzleStone(13, false));
|
||
Register("st-puzzle-nw", new PuzzleStone(10, false));
|
||
Register("st-puzzle-es", new PuzzleStone(7, false));
|
||
Register("st-puzzle-sw", new PuzzleStone(4, false));
|
||
Register("st-puzzle-ns", new PuzzleStone(11, false));
|
||
Register("st-puzzle-ew", new PuzzleStone(6, false));
|
||
Register("st-puzzle-nes", new PuzzleStone(15, false));
|
||
Register("st-puzzle-new", new PuzzleStone(14, false));
|
||
Register("st-puzzle-nsw", new PuzzleStone(12, false));
|
||
Register("st-puzzle-esw", new PuzzleStone(8, false));
|
||
Register("st-puzzle-nesw", new PuzzleStone(16, false));
|
||
|
||
// Oxyd1 compatible puzzle stones:
|
||
Register("st-puzzle2-hollow", new PuzzleStone(1, true));
|
||
Register("st-puzzle2-n", new PuzzleStone(9, true));
|
||
Register("st-puzzle2-e", new PuzzleStone(5, true));
|
||
Register("st-puzzle2-s", new PuzzleStone(3, true));
|
||
Register("st-puzzle2-w", new PuzzleStone(2, true));
|
||
Register("st-puzzle2-ne", new PuzzleStone(13, true));
|
||
Register("st-puzzle2-nw", new PuzzleStone(10, true));
|
||
Register("st-puzzle2-es", new PuzzleStone(7, true));
|
||
Register("st-puzzle2-sw", new PuzzleStone(4, true));
|
||
Register("st-puzzle2-ns", new PuzzleStone(11, true));
|
||
Register("st-puzzle2-ew", new PuzzleStone(6, true));
|
||
Register("st-puzzle2-nes", new PuzzleStone(15, true));
|
||
Register("st-puzzle2-new", new PuzzleStone(14, true));
|
||
Register("st-puzzle2-nsw", new PuzzleStone(12, true));
|
||
Register("st-puzzle2-esw", new PuzzleStone(8, true));
|
||
Register("st-puzzle2-nesw", new PuzzleStone(16, true));
|
||
|
||
//Register("st-bigbrick", new BigBrick(1)); // use st-brick instead
|
||
Register("st-bigbrick-n", new BigBrick(9));
|
||
Register("st-bigbrick-e", new BigBrick(5));
|
||
Register("st-bigbrick-s", new BigBrick(3));
|
||
Register("st-bigbrick-w", new BigBrick(2));
|
||
Register("st-bigbrick-ne", new BigBrick(13));
|
||
Register("st-bigbrick-nw", new BigBrick(10));
|
||
Register("st-bigbrick-es", new BigBrick(7));
|
||
Register("st-bigbrick-sw", new BigBrick(4));
|
||
Register("st-bigbrick-ns", new BigBrick(11));
|
||
Register("st-bigbrick-ew", new BigBrick(6));
|
||
Register("st-bigbrick-nes", new BigBrick(15));
|
||
Register("st-bigbrick-new", new BigBrick(14));
|
||
Register("st-bigbrick-nsw", new BigBrick(12));
|
||
Register("st-bigbrick-esw", new BigBrick(8));
|
||
Register("st-bigbrick-nesw", new BigBrick(16));
|
||
|
||
//Register("st-bigbluesand", new BigBlueSand(1)); // use st-blue-sand instead
|
||
Register("st-bigbluesand-n", new BigBlueSand(9));
|
||
Register("st-bigbluesand-e", new BigBlueSand(5));
|
||
Register("st-bigbluesand-s", new BigBlueSand(3));
|
||
Register("st-bigbluesand-w", new BigBlueSand(2));
|
||
Register("st-bigbluesand-ne", new BigBlueSand(13));
|
||
Register("st-bigbluesand-nw", new BigBlueSand(10));
|
||
Register("st-bigbluesand-es", new BigBlueSand(7));
|
||
Register("st-bigbluesand-sw", new BigBlueSand(4));
|
||
Register("st-bigbluesand-ns", new BigBlueSand(11));
|
||
Register("st-bigbluesand-ew", new BigBlueSand(6));
|
||
Register("st-bigbluesand-nes", new BigBlueSand(15));
|
||
Register("st-bigbluesand-new", new BigBlueSand(14));
|
||
Register("st-bigbluesand-nsw", new BigBlueSand(12));
|
||
Register("st-bigbluesand-esw", new BigBlueSand(8));
|
||
Register("st-bigbluesand-nesw", new BigBlueSand(16));
|
||
|
||
Register ("st-rotator-right", new RotatorStone(true, false));
|
||
Register ("st-rotator-left", new RotatorStone(false, false));
|
||
Register ("st-rotator_move-right", new RotatorStone(true, true));
|
||
Register ("st-rotator_move-left", new RotatorStone(false, true));
|
||
|
||
Register(new ShogunStone);
|
||
Register("st-shogun-s", new ShogunStone(1));
|
||
Register("st-shogun-m", new ShogunStone(2));
|
||
Register("st-shogun-sm", new ShogunStone(3));
|
||
Register("st-shogun-l", new ShogunStone(4));
|
||
Register("st-shogun-sl", new ShogunStone(5));
|
||
Register("st-shogun-ml", new ShogunStone(6));
|
||
Register("st-shogun-sml", new ShogunStone(7));
|
||
|
||
Register(new StoneImpulseStone);
|
||
|
||
Register (new Turnstile_Pivot); // oxyd-comaptible
|
||
Register (new Turnstile_Pivot_Green); // not oxyd-comaptible
|
||
Register (new Turnstile_N);
|
||
Register (new Turnstile_S);
|
||
Register (new Turnstile_E);
|
||
Register (new Turnstile_W);
|
||
|
||
Register("st-chess_black", new ChessStone(0));
|
||
Register("st-chess_white", new ChessStone(1));
|
||
|
||
Register("st-lightpassenger", new LightPassengerStone(true));
|
||
Register("st-lightpassenger_off", new LightPassengerStone(false));
|
||
}
|