Merge branch 'onupdate'
[Tsunagari.git] / src / entity.cpp
blob9ca8d211d824380ff82e08ed830da02591f2adcc
1 /*********************************
2 ** Tsunagari Tile Engine **
3 ** entity.cpp **
4 ** Copyright 2011-2012 OmegaSDG **
5 *********************************/
7 #include <math.h>
9 #include <boost/algorithm/string.hpp> // for iequals
10 #include <boost/foreach.hpp>
11 #include <Gosu/Image.hpp>
12 #include <Gosu/Math.hpp>
13 #include <Gosu/Timing.hpp>
15 #include "area.h"
16 #include "config.h"
17 #include "entity.h"
18 #include "log.h"
19 #include "python.h"
20 #include "resourcer.h"
21 #include "window.h"
22 #include "xml.h"
24 #define ASSERT(x) if (!(x)) return false
26 static std::string directions[][3] = {
27 {"up-left", "up", "up-right"},
28 {"left", "", "right"},
29 {"down-left", "down", "down-right"},
33 Entity::Entity()
34 : redraw(true),
35 phase(NULL),
36 phaseName(""),
37 speedMul(1.0),
38 moving(false),
39 stillMoving(false),
40 nowalkFlags(TILE_NOWALK | TILE_NOWALK_NPC),
41 area(NULL),
42 r(0.0, 0.0, 0.0),
43 frozen(false)
47 Entity::~Entity()
51 bool Entity::init(const std::string& descriptor)
53 this->descriptor = descriptor;
54 return processDescriptor();
57 void Entity::draw()
59 int millis = GameWindow::instance().time();
60 phase->updateFrame(millis);
61 phase->frame()->draw(doff.x + r.x, doff.y + r.y, r.z);
62 redraw = false;
65 bool Entity::needsRedraw() const
67 int millis = GameWindow::instance().time();
68 return redraw || phase->needsRedraw(millis);
72 static double angleFromXY(double x, double y)
74 double angle = 0.0;
76 // Moving at an angle
77 if (x != 0 && y != 0) {
78 angle = atan(y / x);
79 if (y < 0 && x < 0)
81 else if (y < 0 && x > 0)
82 angle += M_PI;
83 else if (y > 0 && x < 0)
84 angle += M_PI*2;
85 else if (y > 0 && x > 0)
86 angle += M_PI;
89 // Moving straight
90 else {
91 if (x < 0)
92 angle = 0;
93 else if (x > 0)
94 angle = M_PI;
95 else if (y < 0)
96 angle = M_PI_2;
97 else if (y > 0)
98 angle = 3*M_PI_2;
101 return angle;
104 void Entity::update(unsigned long dt)
106 updateScripts();
107 switch (conf.moveMode) {
108 case TURN:
109 updateTurn(dt);
110 break;
111 case TILE:
112 updateTile(dt);
113 break;
114 case NOTILE:
115 updateNoTile(dt);
116 break;
120 void Entity::updateTurn(unsigned long)
122 // Entities don't do anything in TILE mode.
125 void Entity::updateTile(unsigned long dt)
127 if (!moving)
128 return;
130 redraw = true;
131 double traveled = speed * (double)dt;
132 double destDist = Gosu::distance(r.x, r.y, destCoord.x, destCoord.y);
133 if (destDist <= traveled) {
134 r = destCoord;
135 moving = false;
136 postMove();
137 if (moving) {
138 // Time rollover.
139 double perc = 1.0 - destDist/traveled;
140 unsigned long remt = (unsigned long)(perc * (double)dt);
141 update(remt);
144 else {
145 double angle = angleFromXY(r.x - destCoord.x,
146 destCoord.y - r.y);
147 double dx = cos(angle);
148 double dy = -sin(angle);
150 // Fix inaccurate trig functions. (Why do we have to do this!??)
151 if (-1e-10 < dx && dx < 1e-10)
152 dx = 0.0;
153 if (-1e-10 < dy && dy < 1e-10)
154 dy = 0.0;
156 r.x += dx * traveled;
157 r.y += dy * traveled;
161 void Entity::updateNoTile(unsigned long)
163 // TODO
166 const std::string Entity::getFacing() const
168 return directionStr(facing);
171 bool Entity::setPhase(const std::string& name)
173 AnimationMap::iterator it;
174 it = phases.find(name);
175 if (it == phases.end()) {
176 Log::err(descriptor, "phase '" + name + "' not found");
177 return false;
179 Animation* newPhase = &it->second;
180 if (phase != newPhase) {
181 int now = GameWindow::instance().time();
182 phase = newPhase;
183 phase->startOver(now);
184 phaseName = name;
185 redraw = true;
186 return true;
188 return false;
191 std::string Entity::getPhase() const
193 return phaseName;
196 rcoord Entity::getPixelCoord() const
198 return r;
201 icoord Entity::getTileCoords_i() const
203 return area->virt2phys(r);
206 vicoord Entity::getTileCoords_vi() const
208 return area->virt2virt(r);
211 void Entity::setTileCoords(int x, int y)
213 vicoord virt(x, y, r.z);
214 if (!area->inBounds(virt))
215 return;
216 redraw = true;
217 r = area->virt2virt(virt);
220 void Entity::setTileCoords(int x, int y, double z)
222 vicoord virt(x, y, z);
223 if (!area->inBounds(virt))
224 return;
225 redraw = true;
226 r = area->virt2virt(virt);
229 void Entity::setTileCoords(icoord phys)
231 if (!area->inBounds(phys))
232 return;
233 redraw = true;
234 r = area->phys2virt_r(phys);
237 void Entity::setTileCoords(vicoord virt)
239 if (!area->inBounds(virt))
240 return;
241 redraw = true;
242 r = area->virt2virt(virt);
245 bool Entity::isMoving() const
247 return moving || stillMoving;
250 void Entity::moveByTile(int x, int y)
252 moveByTile(ivec2(x, y));
255 void Entity::moveByTile(ivec2 delta)
257 if (moving)
258 return;
259 setFacing(delta);
261 std::vector<icoord> tiles = frontTiles();
262 BOOST_FOREACH(const icoord& tile, tiles) {
263 if (canMove(tile)) {
264 preMove();
265 return;
267 else
268 setPhase(directionStr(facing));
272 Area* Entity::getArea()
274 return area;
277 void Entity::setArea(Area* a)
279 area = a;
280 calcDoff();
281 setSpeed(speedMul); // Calculate new speed based on tile size.
284 double Entity::getSpeed() const
286 return speedMul;
289 void Entity::setSpeed(double multiplier)
291 speedMul = multiplier;
292 if (area) {
293 double tilesPerSecond = area->getTileDimensions().x / 1000.0;
294 speed = baseSpeed * speedMul * tilesPerSecond;
298 void Entity::addOnUpdateListener(boost::python::object callable)
300 updateHooks.push_back(callable);
303 Tile& Entity::getTile() const
305 return area->getTile(getTileCoords_i());
308 Tile& Entity::getTile()
310 return area->getTile(getTileCoords_i());
313 void Entity::setFrozen(bool b)
315 frozen = b;
318 bool Entity::getFrozen()
320 return frozen;
323 FlagManip Entity::exemptManip()
325 return FlagManip(&nowalkExempt);
328 std::vector<icoord> Entity::frontTiles() const
330 std::vector<icoord> tiles;
331 icoord dest = getTileCoords_i();
332 dest += icoord(facing.x, facing.y, 0);
334 boost::optional<double> layermod = getTile().layermodAt(facing);
335 if (layermod)
336 dest = area->virt2phys(vicoord(dest.x, dest.y, *layermod));
337 tiles.push_back(dest);
338 return tiles;
341 void Entity::calcDoff()
343 // X-axis is centered on tile.
344 doff.x = (area->getTileDimensions().x - imgw) / 2;
345 // Y-axis is aligned with bottom of tile.
346 doff.y = area->getTileDimensions().y - imgh - 1;
349 SampleRef Entity::getSound(const std::string& name) const
351 SampleMap::const_iterator it;
352 it = sounds.find(name);
353 if (it != sounds.end())
354 return it->second;
355 else
356 return SampleRef();
359 ivec2 Entity::setFacing(ivec2 facing)
361 this->facing = ivec2(
362 Gosu::clamp(facing.x, -1, 1),
363 Gosu::clamp(facing.y, -1, 1)
365 return this->facing;
368 const std::string& Entity::directionStr(ivec2 facing) const
370 return directions[facing.y+1][facing.x+1];
373 bool Entity::canMove(icoord dest)
375 bool inBounds;
376 icoord delta = dest;
377 delta -= getTileCoords_i();
378 ivec2 dxy(delta.x, delta.y);
379 if (!(inBounds = area->inBounds(dest)) &&
380 !(delta.z == 0 && getTile().exitAt(dxy)))
381 // The tile is off the map.
382 return false;
383 destCoord = area->phys2virt_r(dest);
384 if (inBounds) {
385 destTile = &area->getTile(dest);
386 return !nowalked(*destTile);
388 else {
389 destTile = NULL;
390 return true;
394 bool Entity::nowalked(Tile& t)
396 unsigned flags = nowalkFlags & ~nowalkExempt;
398 if (flags & TILE_NOWALK) {
399 if (t.hasFlag(TILE_NOWALK))
400 return true;
402 if (flags & TILE_NOWALK_PLAYER) {
403 if (t.hasFlag(TILE_NOWALK_PLAYER))
404 return true;
406 if (flags & TILE_NOWALK_NPC) {
407 if (t.hasFlag(TILE_NOWALK_NPC))
408 return true;
410 return false;
413 void Entity::preMove()
415 fromCoord = r;
416 fromTile = &getTile();
418 rcoord d = destCoord;
419 d -= fromCoord;
420 deltaCoord = area->virt2virt(d);
422 moving = true;
424 // Set z right away so that we're on-level with the square we're
425 // entering.
426 r.z = destCoord.z;
428 // Start moving animation.
429 switch (conf.moveMode) {
430 case TURN:
431 break;
432 case TILE:
433 case NOTILE:
434 setPhase("moving " + getFacing());
435 break;
438 // Process triggers.
439 tileExitScript();
440 fromTile->onLeaveScripts(this);
442 SampleRef step = getSound("step");
443 if (step)
444 step->play();
446 if (conf.moveMode == TURN) {
447 // Movement is instantaneous.
448 redraw = true;
449 r = destCoord;
450 postMove();
454 void Entity::postMove()
456 moving = false;
458 if (destTile) {
459 boost::optional<double> layermod = getTile().layermods[EXIT_NORMAL];
460 if (layermod)
461 r.z = *layermod;
464 // Stop moving animation.
465 if (!stillMoving)
466 setPhase(getFacing());
468 // Process triggers.
469 if (destTile) {
470 destTile->onEnterScripts(this);
471 tileEntryScript();
474 // TODO: move teleportation here
476 * if (onExit()) {
477 * leaveTile();
478 * moveArea(getExit());
479 * postMoveScript();
480 * enterTile();
485 void Entity::updateScripts()
487 BOOST_FOREACH(ScriptInst& script, updateHooks) {
488 pythonSetGlobal("Entity", this);
489 pythonSetGlobal("Tile", &getTile());
490 script.invoke();
494 void Entity::tileExitScript()
496 BOOST_FOREACH(ScriptInst& script, tileExitHooks) {
497 pythonSetGlobal("Entity", this);
498 pythonSetGlobal("Tile", &getTile());
499 script.invoke();
503 void Entity::tileEntryScript()
505 BOOST_FOREACH(ScriptInst& script, tileEntryHooks) {
506 pythonSetGlobal("Entity", this);
507 pythonSetGlobal("Tile", &getTile());
508 script.invoke();
514 * DESCRIPTOR CODE BELOW
517 bool Entity::processDescriptor()
519 Resourcer* rc = Resourcer::instance();
520 XMLRef doc = rc->getXMLDoc(descriptor, "entity.dtd");
521 if (!doc)
522 return false;
523 const XMLNode root = doc->root(); // <entity>
524 if (!root)
525 return false;
527 for (XMLNode node = root.childrenNode(); node; node = node.next()) {
528 if (node.is("speed")) {
529 ASSERT(node.doubleContent(&baseSpeed));
530 setSpeed(speedMul); // Calculate speed from tile size.
531 } else if (node.is("sprite")) {
532 ASSERT(processSprite(node.childrenNode()));
533 } else if (node.is("sounds")) {
534 ASSERT(processSounds(node.childrenNode()));
535 } else if (node.is("scripts")) {
536 ASSERT(processScripts(node.childrenNode()));
539 return true;
542 bool Entity::processSprite(XMLNode node)
544 Resourcer* rc = Resourcer::instance();
545 TiledImage tiles;
546 for (; node; node = node.next()) {
547 if (node.is("sheet")) {
548 std::string imageSheet = node.content();
549 ASSERT(node.intAttr("tile_width", &imgw) &&
550 node.intAttr("tile_height", &imgh));
551 ASSERT(rc->getTiledImage(tiles, imageSheet,
552 imgw, imgh, false));
553 } else if (node.is("phases")) {
554 ASSERT(processPhases(node.childrenNode(), tiles));
557 return true;
560 bool Entity::processPhases(XMLNode node, const TiledImage& tiles)
562 for (; node; node = node.next())
563 if (node.is("phase"))
564 ASSERT(processPhase(node, tiles));
565 return true;
568 bool Entity::processPhase(const XMLNode node, const TiledImage& tiles)
570 /* Each phase requires a 'name'. Additionally,
571 * one of either 'pos' or 'speed' is needed.
572 * If speed is used, we have sub-elements. We
573 * can't have both pos and speed.
575 const std::string name = node.attr("name");
576 if (name.empty()) {
577 Log::err(descriptor, "<phase> name attribute is empty");
578 return false;
581 const std::string posStr = node.attr("pos");
582 const std::string speedStr = node.attr("speed");
584 if (posStr.size() && speedStr.size()) {
585 Log::err(descriptor, "pos and speed attributes in "
586 "phase element are mutually exclusive");
587 return false;
588 } else if (posStr.empty() && speedStr.empty()) {
589 Log::err(descriptor, "must have pos or speed attribute "
590 "in phase element");
591 return false;
594 if (posStr.size()) {
595 int pos;
596 ASSERT(node.intAttr("pos", &pos));
597 if (pos < 0 || (int)tiles.size() < pos) {
598 Log::err(descriptor,
599 "<phase></phase> index out of bounds");
600 return false;
602 phases[name].addFrame(tiles[pos]);
604 else {
605 int speed;
606 ASSERT(node.intAttr("speed", &speed));
608 int len = (int)(1000.0/speed);
609 phases[name].setFrameLen(len);
610 ASSERT(processMembers(node.childrenNode(),
611 phases[name], tiles));
614 return true;
617 bool Entity::processMembers(XMLNode node, Animation& anim,
618 const TiledImage& tiles)
620 for (; node; node = node.next())
621 if (node.is("member"))
622 ASSERT(processMember(node, anim, tiles));
623 return true;
626 bool Entity::processMember(const XMLNode node, Animation& anim,
627 const TiledImage& tiles)
629 int pos;
630 ASSERT(node.intAttr("pos", &pos));
631 if (pos < 0 || (int)tiles.size() < pos) {
632 Log::err(descriptor, "<member></member> index out of bounds");
633 return false;
635 anim.addFrame(tiles[pos]);
636 return true;
639 bool Entity::processSounds(XMLNode node)
641 for (; node; node = node.next())
642 if (node.is("sound"))
643 ASSERT(processSound(node));
644 return true;
647 bool Entity::processSound(const XMLNode node)
649 const std::string name = node.attr("name");
650 const std::string filename = node.content();
651 if (name.empty()) {
652 Log::err(descriptor, "<sound> name attribute is empty");
653 return false;
654 } else if (filename.empty()) {
655 Log::err(descriptor, "<sound></sound> is empty");
656 return false;
659 Resourcer* rc = Resourcer::instance();
660 SampleRef s = rc->getSample(filename);
661 if (s)
662 sounds[name] = s;
663 return true;
666 bool Entity::processScripts(XMLNode node)
668 for (; node; node = node.next())
669 if (node.is("script"))
670 ASSERT(processScript(node));
671 return true;
674 bool Entity::processScript(const XMLNode node)
676 const std::string trigger = node.attr("trigger");
677 const std::string filename = node.content();
678 if (trigger.empty()) {
679 Log::err(descriptor, "<script> trigger attribute is empty");
680 return false;
681 } else if (filename.empty()) {
682 Log::err(descriptor, "<script></script> is empty");
683 return false;
686 Resourcer* rc = Resourcer::instance();
687 if (!rc->resourceExists(filename)) {
688 Log::err(descriptor,
689 "script not found: " + filename);
690 return false;
693 if (!addScript(trigger, filename)) {
694 Log::err(descriptor,
695 "unrecognized script trigger: " + trigger);
696 return false;
699 return true;
702 bool Entity::addScript(const std::string& trigger, const std::string& filename)
704 if (boost::iequals(trigger, "on_update")) {
705 updateHooks.push_back(ScriptInst(filename));
706 return true;
708 if (boost::equals(trigger, "on_tile_entry")) {
709 tileEntryHooks.push_back(ScriptInst(filename));
710 return true;
712 if (boost::iequals(trigger, "on_tile_exit")) {
713 tileExitHooks.push_back(ScriptInst(filename));
714 return true;
716 return false;
720 void exportEntity()
722 using namespace boost::python;
724 class_<Entity>("Entity")
725 .def("init", &Entity::init)
726 .add_property("frozen", &Entity::getFrozen, &Entity::setFrozen)
727 .add_property("phase", &Entity::getPhase, &Entity::setPhase)
728 .add_property("area",
729 make_function(&Entity::getArea,
730 return_value_policy<reference_existing_object>()),
731 &Entity::setArea)
732 .add_property("tile", make_function(
733 static_cast<Tile& (Entity::*) ()> (&Entity::getTile),
734 return_value_policy<reference_existing_object>()))
735 .add_property("coords", &Entity::getTileCoords_vi)
736 .add_property("speed", &Entity::getSpeed, &Entity::setSpeed)
737 .add_property("moving", &Entity::isMoving)
738 .add_property("exempt", &Entity::exemptManip)
739 .def("set_coords",
740 static_cast<void (Entity::*) (int,int,double)>
741 (&Entity::setTileCoords))
742 .def("move", static_cast<void (Entity::*) (int,int)>
743 (&Entity::moveByTile))
744 .def("teleport", static_cast<void (Entity::*) (int,int)>
745 (&Entity::setTileCoords))
746 .def("add_on_update_listener", &Entity::addOnUpdateListener)
747 .def("move",
748 static_cast<void (Entity::*) (int,int)>
749 (&Entity::moveByTile));