/* * 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., * 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. * * $Id: stones_complex.cc,v 1.59 2004/05/22 13:04:27 dheck Exp $ */ #include "laser.hh" #include "sound.hh" #include "server.hh" #include "player.hh" #include "stones_internal.hh" #include "px/tools.hh" #include #include #include using namespace std; using namespace world; using namespace stones; /* -------------------- RotatorStone -------------------- */ namespace { class RotatorStone : public Stone { public: RotatorStone(bool clockwise_, bool movable_) : Stone("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; } 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::wielded_item_is(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); } void on_laserhit(Direction) { clockwise = !clockwise; init_model(); } }; 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 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& 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() { 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); GridPos oldpos = move (get_pos(), reverse(m_movedir)); // remove the disappearing half of the PullStone : PullStone *oldStone = dynamic_cast(GetStone(oldpos)); assert(oldStone); 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::iterator e = pull_info->get_actors().end(); for (list::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 found_actors; const double range_one_field = 1.415; // approx. 1 field [ > sqrt(1+1) ] GetActorsInRange(newPos.center(), range_one_field, found_actors); vector::iterator e = found_actors.end(); for (vector::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 warp((oldPos.x-newPos.x)*0.4, (oldPos.y-newPos.y)*0.4); // half move vector (of actor) V2 vel = ai->vel; V2 mid_dest = ai->pos+warp; // position during move WarpActor(actor, mid_dest[0], mid_dest[1], false); // if (length(vel) < 15.0) // speed up actor // vel += 1.7*warp; 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(); void 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); } void 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(); } } void OneWayBase::actor_hit(const StoneContact &sc) { Direction o=get_orientation(); if (has_dir(contact_faces(sc), o)) { if (player::wielded_item_is(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(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::wielded_item_is(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); } void message(const string& msg, const Value &val) { if (msg == "direction" && state != FALLING) { set_dir (to_direction(val)); init_model(); } } }; } /* -------------------- 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); break; } } void 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); } } 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()); } } void message(const string &msg, const Value &) { if (msg == "trigger") { if (state == INACTIVE) { state = ACTIVE; init_model(); } } } 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::wielded_item_is(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()+px::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) {} }; } /* -------------------- 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
\image html st-puzzletempl_0001.png "PUZ_0000" \image html st-puzzletempl_0002.png "PUZ_0001" \image html st-puzzletempl_0003.png "PUZ_0010" \image html st-puzzletempl_0004.png "PUZ_0011"
\image html st-puzzletempl_0005.png "PUZ_0100" \image html st-puzzletempl_0006.png "PUZ_0101" \image html st-puzzletempl_0007.png "PUZ_0110" \image html st-puzzletempl_0008.png "PUZ_0111"
\image html st-puzzletempl_0009.png "PUZ_1000" \image html st-puzzletempl_0010.png "PUZ_1001" \image html st-puzzletempl_0011.png "PUZ_1010" \image html st-puzzletempl_0012.png "PUZ_1011"
\image html st-puzzletempl_0013.png "PUZ_1100" \image html st-puzzletempl_0014.png "PUZ_1101" \image html st-puzzletempl_0015.png "PUZ_1110" \image html st-puzzletempl_0016.png "PUZ_1111"
*/ namespace { class PuzzleStone : public ConnectiveStone, public TimeHandler, public world::PhotoCell { INSTANCELISTOBJ(PuzzleStone); public: PuzzleStone(int connections, bool oxyd1_compatible_); private: typedef vector Cluster; /* ---------- Private methods ---------- */ bool oxyd1_compatible() const { return int_attrib("oxyd") != 0; } static bool visit_dir(vector &stack, GridPos curpos, Direction dir); static void visit_adjacent(vector& 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 ---------- */ void 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 &stack, GridPos curpos, Direction dir) { GridPos newpos = move(curpos, dir); PuzzleStone *pz = dynamic_cast(GetStone(newpos)); if (!pz) 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; ivisited=false; vector pos_stack; bool is_complete = true; pos_stack.push_back(get_pos()); this->visited = true; while (!pos_stack.empty()) { GridPos curpos = pos_stack.back(); pos_stack.pop_back(); PuzzleStone *pz = dynamic_cast(GetStone(curpos)); assert(pz); 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); if (has_dir(cfaces, EAST)) is_complete &= visit_dir(pos_stack, curpos, EAST); if (has_dir(cfaces, SOUTH)) is_complete &= visit_dir(pos_stack, curpos, SOUTH); if (has_dir(cfaces, WEST)) is_complete &= visit_dir(pos_stack, curpos, WEST); } return is_complete; } void PuzzleStone::visit_adjacent (vector& stack, GridPos curpos, Direction dir, int wanted_oxyd_attrib) { GridPos newpos = move(curpos, dir); if (PuzzleStone *pz = dynamic_cast(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; ivisited=false; vector 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); GridPos p = startpos; while (Stone *puzz = dynamic_cast(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; iis_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 clusterstones; for (unsigned i=0; ion_move(); } } server::IncMoveCounter(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) { unsigned size = c.size(); if (size > 1) { int cn = GetStone(c[size-1])->int_attrib("connections"); for (unsigned i=size-1; i>0; --i) { PuzzleStone *st = dynamic_cast (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 (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(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(); } void 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()); int size = c.size(); // warning("received 'scramble'. dir=%s size=%i", to_suffix(dir).c_str(), size); if (size >= 2) { int count = IntegerRand(0, size-1); while (count--) rotate_cluster(c); } else { warning("useless scramble (cluster size=%i)", size); } } } 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(impulse.sender)) actor_with_wand = player::wielded_item_is(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); 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()); 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(GetStone(all[0])); assert(pz); 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) { px::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); 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::wielded_item_is(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) ; void 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; } }; } void 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); } 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)); } private: Holes get_holes() const; void notify_item(); void message(const string &m, const Value &) { if (m == "init") { // request from ShogunDot (if set _after_ ShogunStone) notify_item(); } } 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(px::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; assert(0); } 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(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::SoundEvent ("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; void message(const string &m, const Value &value) { if (m=="trigger") { incoming = (value.get_type() == Value::DOUBLE) ? Direction(value.get_double()+0.1) : NODIR; change_state(PULSING); } else if (m == "signal" && to_double (value) != 0) { incoming = NODIR; change_state (PULSING); } } 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(impulse.sender); if (hitman && player::wielded_item_is(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
\image html st-oxyda.png "flavor A" \image html st-oxydb.png "flavor B" \image html st-oxydc.png "flavor C" \image html st-oxydd.png "flavor D"
*/ namespace { class OxydStone : public PhotoStone { INSTANCELISTOBJ(OxydStone); public: OxydStone(); static void shuffle_colors(); 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"; } void 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"); } void OxydStone::message(const string &m, const Value &val) { if (m=="closeall") { for (unsigned i=0; ichange_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\n"; } } } void OxydStone::shuffle_colors() { vector closed_oxyds; unsigned isize = instances.size(); for (unsigned i=0; istate == CLOSED) { closed_oxyds.push_back(i); } } unsigned size = closed_oxyds.size(); if (size>1) { for (unsigned i = 0; i= 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(); } 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 void 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(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") {} }; /* ** */ class Turnstile_Corner : public Stone { CLONEOBJ(Turnstile_Corner); void init_model() { set_anim ("st-turnstile-corner"); } void animcb() { KillStone(get_pos()); } public: Turnstile_Corner() : Stone("st-turnstile-corner") {} }; } /* -------------------- 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; } void 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); } } DirectionBits Turnstile_Pivot_Base::arms_present() const { DirectionBits arms = NODIRBIT; GridPos p = get_pos(); if (dynamic_cast(GetStone(move(p, NORTH)))) px::set_flags (arms, NORTHBIT); if (dynamic_cast(GetStone(move(p, SOUTH)))) px::set_flags (arms, SOUTHBIT); if (dynamic_cast(GetStone(move(p, EAST)))) px::set_flags (arms, EASTBIT); if (dynamic_cast(GetStone(move(p, WEST)))) px::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) { if (clockwise) sound_event ("turnstileright"); else sound_event ("turnstileleft"); // if (dynamic_cast(impulse_sender)) { // } sound_event("movesmall"); active = true; set_anim(anim()); rotate_arms(arms, clockwise); handleActorsAndItems(clockwise, impulse_sender); PerformAction (this, 1-clockwise); 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]; } } 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 actorsInRange; // tested range is sqrt(sqr(1.5)+sqr(0.5)) // = (radius swept by turnstile) + 19/64 ( = max. actor radius) if (!GetActorsInRange(pv_pos.center(), 1.879, actorsInRange)) return; vector::iterator i = actorsInRange.begin(), end = actorsInRange.end(); for (; i != end; ++i) { Actor *ac = *i; 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 if (dx<-1 || dx>1 || dy<-1 || dy>1) 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] = { { 6, 0, 0, 2, 2, 4, 4, 6 }, // anticlockwise { 2, 2, 4, 4, 6, 6, 0, 0 }, // clockwise { 6, -1, 0, -1, 2, -1, 4, -1 }, // anticlockwise (oxyd-compatible) { 2, -1, 4, -1, 6, -1, 0, -1 }, // clockwise (oxyd-compatible) }; bool compatible = oxyd_compatible(); int idx_target = rot_index[clockwise+2*compatible][idx_source]; // destination index bool do_warp = arm_seen[idx_source]; // 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(impulse_sender)); if (!do_warp && arm_seen[idx_source]) SendMessage(ac, "shatter"); // hit by an arm } if (!do_warp) continue; GridPos ac_target_pos(pv_pos.x+to_x[idx_target], pv_pos.y+to_y[idx_target]); 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(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(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 (player::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(); it->drop(sc.actor, p); } } } } GridPos MailStone::find_pipe_endpoint() { GridPos p = get_pos(); Direction move_dir = m_dir; 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; } return p; } // -------------------------------------------------------------------------------- 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)); 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-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); }