FEATURE: rough collision support between Entities
[Tsunagari.git] / src / entity.cpp
blob62cdcf3b84b57fddd07d1b7f156b038d650dbdc4
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 | TILE_NOWALK_BCO_ENTITY),
41 nowalkExempt(0),
42 area(NULL),
43 r(0.0, 0.0, 0.0),
44 frozen(false)
48 Entity::~Entity()
52 bool Entity::init(const std::string& descriptor)
54 this->descriptor = descriptor;
55 return processDescriptor();
58 void Entity::draw()
60 int millis = GameWindow::instance().time();
61 phase->updateFrame(millis);
62 phase->frame()->draw(doff.x + r.x, doff.y + r.y, r.z);
63 redraw = false;
66 bool Entity::needsRedraw() const
68 int millis = GameWindow::instance().time();
69 return redraw || phase->needsRedraw(millis);
73 static double angleFromXY(double x, double y)
75 double angle = 0.0;
77 // Moving at an angle
78 if (x != 0 && y != 0) {
79 angle = atan(y / x);
80 if (y < 0 && x < 0)
82 else if (y < 0 && x > 0)
83 angle += M_PI;
84 else if (y > 0 && x < 0)
85 angle += M_PI*2;
86 else if (y > 0 && x > 0)
87 angle += M_PI;
90 // Moving straight
91 else {
92 if (x < 0)
93 angle = 0;
94 else if (x > 0)
95 angle = M_PI;
96 else if (y < 0)
97 angle = M_PI_2;
98 else if (y > 0)
99 angle = 3*M_PI_2;
102 return angle;
105 void Entity::update(unsigned long dt)
107 updateScripts();
108 switch (conf.moveMode) {
109 case TURN:
110 updateTurn(dt);
111 break;
112 case TILE:
113 updateTile(dt);
114 break;
115 case NOTILE:
116 updateNoTile(dt);
117 break;
121 void Entity::updateTurn(unsigned long)
123 // Entities don't do anything in TILE mode.
126 void Entity::updateTile(unsigned long dt)
128 if (!moving)
129 return;
131 redraw = true;
132 double traveled = speed * (double)dt;
133 double destDist = Gosu::distance(r.x, r.y, destCoord.x, destCoord.y);
134 if (destDist <= traveled) {
135 r = destCoord;
136 moving = false;
137 postMove();
138 if (moving) {
139 // Time rollover.
140 double perc = 1.0 - destDist/traveled;
141 unsigned long remt = (unsigned long)(perc * (double)dt);
142 update(remt);
145 else {
146 double angle = angleFromXY(r.x - destCoord.x,
147 destCoord.y - r.y);
148 double dx = cos(angle);
149 double dy = -sin(angle);
151 // Fix inaccurate trig functions. (Why do we have to do this!??)
152 if (-1e-10 < dx && dx < 1e-10)
153 dx = 0.0;
154 if (-1e-10 < dy && dy < 1e-10)
155 dy = 0.0;
157 r.x += dx * traveled;
158 r.y += dy * traveled;
162 void Entity::updateNoTile(unsigned long)
164 // TODO
167 const std::string Entity::getFacing() const
169 return directionStr(facing);
172 bool Entity::setPhase(const std::string& name)
174 AnimationMap::iterator it;
175 it = phases.find(name);
176 if (it == phases.end()) {
177 Log::err(descriptor, "phase '" + name + "' not found");
178 return false;
180 Animation* newPhase = &it->second;
181 if (phase != newPhase) {
182 int now = GameWindow::instance().time();
183 phase = newPhase;
184 phase->startOver(now);
185 phaseName = name;
186 redraw = true;
187 return true;
189 return false;
192 std::string Entity::getPhase() const
194 return phaseName;
197 rcoord Entity::getPixelCoord() const
199 return r;
202 icoord Entity::getTileCoords_i() const
204 return area->virt2phys(r);
207 vicoord Entity::getTileCoords_vi() const
209 return area->virt2virt(r);
212 void Entity::setTileCoords(int x, int y)
214 vicoord virt(x, y, r.z);
215 if (!area->inBounds(virt))
216 return;
217 redraw = true;
218 r = area->virt2virt(virt);
219 occupy(getTile());
222 void Entity::setTileCoords(int x, int y, double z)
224 vicoord virt(x, y, z);
225 if (!area->inBounds(virt))
226 return;
227 redraw = true;
228 r = area->virt2virt(virt);
229 occupy(getTile());
232 void Entity::setTileCoords(icoord phys)
234 if (!area->inBounds(phys))
235 return;
236 redraw = true;
237 r = area->phys2virt_r(phys);
238 occupy(getTile());
241 void Entity::setTileCoords(vicoord virt)
243 if (!area->inBounds(virt))
244 return;
245 redraw = true;
246 r = area->virt2virt(virt);
247 occupy(getTile());
250 bool Entity::isMoving() const
252 return moving || stillMoving;
255 void Entity::moveByTile(int x, int y)
257 moveByTile(ivec2(x, y));
260 void Entity::moveByTile(ivec2 delta)
262 if (moving)
263 return;
264 setFacing(delta);
266 std::vector<icoord> tiles = frontTiles();
267 BOOST_FOREACH(const icoord& tile, tiles) {
268 if (canMove(tile)) {
269 preMove();
270 return;
272 else
273 setPhase(directionStr(facing));
277 Area* Entity::getArea()
279 return area;
282 void Entity::setArea(Area* a)
284 area = a;
285 calcDoff();
286 setSpeed(speedMul); // Calculate new speed based on tile size.
287 occupy(getTile());
290 double Entity::getSpeed() const
292 return speedMul;
295 void Entity::setSpeed(double multiplier)
297 speedMul = multiplier;
298 if (area) {
299 double tilesPerSecond = area->getTileDimensions().x / 1000.0;
300 speed = baseSpeed * speedMul * tilesPerSecond;
304 void Entity::addOnUpdateListener(boost::python::object callable)
306 updateHooks.push_back(callable);
309 Tile& Entity::getTile() const
311 return area->getTile(getTileCoords_i());
314 Tile& Entity::getTile()
316 return area->getTile(getTileCoords_i());
319 void Entity::setFrozen(bool b)
321 frozen = b;
324 bool Entity::getFrozen()
326 return frozen;
329 FlagManip Entity::exemptManip()
331 return FlagManip(&nowalkExempt);
334 std::vector<icoord> Entity::frontTiles() const
336 std::vector<icoord> tiles;
337 icoord dest = getTileCoords_i();
338 dest += icoord(facing.x, facing.y, 0);
340 boost::optional<double> layermod = getTile().layermodAt(facing);
341 if (layermod)
342 dest = area->virt2phys(vicoord(dest.x, dest.y, *layermod));
343 tiles.push_back(dest);
344 return tiles;
347 void Entity::calcDoff()
349 // X-axis is centered on tile.
350 doff.x = (area->getTileDimensions().x - imgw) / 2;
351 // Y-axis is aligned with bottom of tile.
352 doff.y = area->getTileDimensions().y - imgh - 1;
355 SampleRef Entity::getSound(const std::string& name) const
357 SampleMap::const_iterator it;
358 it = sounds.find(name);
359 if (it != sounds.end())
360 return it->second;
361 else
362 return SampleRef();
365 ivec2 Entity::setFacing(ivec2 facing)
367 this->facing = ivec2(
368 Gosu::clamp(facing.x, -1, 1),
369 Gosu::clamp(facing.y, -1, 1)
371 return this->facing;
374 const std::string& Entity::directionStr(ivec2 facing) const
376 return directions[facing.y+1][facing.x+1];
379 bool Entity::canMove(icoord dest)
381 bool inBounds;
382 icoord delta = dest;
383 delta -= getTileCoords_i();
384 ivec2 dxy(delta.x, delta.y);
385 if (!(inBounds = area->inBounds(dest)) &&
386 !(delta.z == 0 && getTile().exitAt(dxy)))
387 // The tile is off the map.
388 return false;
389 destCoord = area->phys2virt_r(dest);
390 if (inBounds) {
391 destTile = &area->getTile(dest);
392 return !nowalked(*destTile);
394 else {
395 destTile = NULL;
396 return true;
400 bool Entity::nowalked(Tile& t)
402 unsigned flags = nowalkFlags & ~nowalkExempt;
403 return t.hasFlag(flags);
406 void Entity::occupy(Tile& t)
408 unsigned& old = getTile().flags;
410 // XXX: When first placing an Entity after creating it, it will be
411 // relocated from its initial position of <0,0,0>. This will break any
412 // NOWALK_BCO_ENTITY put there by another Entity. More generally,
413 // whenever we have a possibility of two Entities being on top of each
414 // other, this will break it.
415 old &= ~TILE_NOWALK_BCO_ENTITY;
416 t.flags |= TILE_NOWALK_BCO_ENTITY;
419 void Entity::preMove()
421 fromCoord = r;
422 fromTile = &getTile();
424 rcoord d = destCoord;
425 d -= fromCoord;
426 deltaCoord = area->virt2virt(d);
428 moving = true;
430 // Set z right away so that we're on-level with the square we're
431 // entering.
432 r.z = destCoord.z;
434 // Start moving animation.
435 switch (conf.moveMode) {
436 case TURN:
437 break;
438 case TILE:
439 case NOTILE:
440 setPhase("moving " + getFacing());
441 break;
444 // Process triggers.
445 tileExitScript();
446 fromTile->onLeaveScripts(this);
448 // Set NOWALK_BCO_ENTITY on the destination tile to "reserve" it,
449 // making it exclusive to us. Do this before we even start animating
450 // on to it.
451 if (destTile)
452 occupy(*destTile);
454 SampleRef step = getSound("step");
455 if (step)
456 step->play();
458 if (conf.moveMode == TURN) {
459 // Movement is instantaneous.
460 redraw = true;
461 r = destCoord;
462 postMove();
466 void Entity::postMove()
468 moving = false;
470 if (destTile) {
471 boost::optional<double> layermod = getTile().layermods[EXIT_NORMAL];
472 if (layermod)
473 r.z = *layermod;
476 // Stop moving animation.
477 if (!stillMoving)
478 setPhase(getFacing());
480 // Process triggers.
481 if (destTile) {
482 destTile->onEnterScripts(this);
483 tileEntryScript();
486 // TODO: move teleportation here
488 * if (onExit()) {
489 * leaveTile();
490 * moveArea(getExit());
491 * postMoveScript();
492 * enterTile();
497 void Entity::updateScripts()
499 BOOST_FOREACH(ScriptInst& script, updateHooks) {
500 pythonSetGlobal("Entity", this);
501 pythonSetGlobal("Tile", &getTile());
502 script.invoke();
506 void Entity::tileExitScript()
508 BOOST_FOREACH(ScriptInst& script, tileExitHooks) {
509 pythonSetGlobal("Entity", this);
510 pythonSetGlobal("Tile", &getTile());
511 script.invoke();
515 void Entity::tileEntryScript()
517 BOOST_FOREACH(ScriptInst& script, tileEntryHooks) {
518 pythonSetGlobal("Entity", this);
519 pythonSetGlobal("Tile", &getTile());
520 script.invoke();
526 * DESCRIPTOR CODE BELOW
529 bool Entity::processDescriptor()
531 Resourcer* rc = Resourcer::instance();
532 XMLRef doc = rc->getXMLDoc(descriptor, "entity.dtd");
533 if (!doc)
534 return false;
535 const XMLNode root = doc->root(); // <entity>
536 if (!root)
537 return false;
539 for (XMLNode node = root.childrenNode(); node; node = node.next()) {
540 if (node.is("speed")) {
541 ASSERT(node.doubleContent(&baseSpeed));
542 setSpeed(speedMul); // Calculate speed from tile size.
543 } else if (node.is("sprite")) {
544 ASSERT(processSprite(node.childrenNode()));
545 } else if (node.is("sounds")) {
546 ASSERT(processSounds(node.childrenNode()));
547 } else if (node.is("scripts")) {
548 ASSERT(processScripts(node.childrenNode()));
551 return true;
554 bool Entity::processSprite(XMLNode node)
556 Resourcer* rc = Resourcer::instance();
557 TiledImage tiles;
558 for (; node; node = node.next()) {
559 if (node.is("sheet")) {
560 std::string imageSheet = node.content();
561 ASSERT(node.intAttr("tile_width", &imgw) &&
562 node.intAttr("tile_height", &imgh));
563 ASSERT(rc->getTiledImage(tiles, imageSheet,
564 imgw, imgh, false));
565 } else if (node.is("phases")) {
566 ASSERT(processPhases(node.childrenNode(), tiles));
569 return true;
572 bool Entity::processPhases(XMLNode node, const TiledImage& tiles)
574 for (; node; node = node.next())
575 if (node.is("phase"))
576 ASSERT(processPhase(node, tiles));
577 return true;
580 bool Entity::processPhase(const XMLNode node, const TiledImage& tiles)
582 /* Each phase requires a 'name'. Additionally,
583 * one of either 'pos' or 'speed' is needed.
584 * If speed is used, we have sub-elements. We
585 * can't have both pos and speed.
587 const std::string name = node.attr("name");
588 if (name.empty()) {
589 Log::err(descriptor, "<phase> name attribute is empty");
590 return false;
593 const std::string posStr = node.attr("pos");
594 const std::string speedStr = node.attr("speed");
596 if (posStr.size() && speedStr.size()) {
597 Log::err(descriptor, "pos and speed attributes in "
598 "phase element are mutually exclusive");
599 return false;
600 } else if (posStr.empty() && speedStr.empty()) {
601 Log::err(descriptor, "must have pos or speed attribute "
602 "in phase element");
603 return false;
606 if (posStr.size()) {
607 int pos;
608 ASSERT(node.intAttr("pos", &pos));
609 if (pos < 0 || (int)tiles.size() < pos) {
610 Log::err(descriptor,
611 "<phase></phase> index out of bounds");
612 return false;
614 phases[name].addFrame(tiles[pos]);
616 else {
617 int speed;
618 ASSERT(node.intAttr("speed", &speed));
620 int len = (int)(1000.0/speed);
621 phases[name].setFrameLen(len);
622 ASSERT(processMembers(node.childrenNode(),
623 phases[name], tiles));
626 return true;
629 bool Entity::processMembers(XMLNode node, Animation& anim,
630 const TiledImage& tiles)
632 for (; node; node = node.next())
633 if (node.is("member"))
634 ASSERT(processMember(node, anim, tiles));
635 return true;
638 bool Entity::processMember(const XMLNode node, Animation& anim,
639 const TiledImage& tiles)
641 int pos;
642 ASSERT(node.intAttr("pos", &pos));
643 if (pos < 0 || (int)tiles.size() < pos) {
644 Log::err(descriptor, "<member></member> index out of bounds");
645 return false;
647 anim.addFrame(tiles[pos]);
648 return true;
651 bool Entity::processSounds(XMLNode node)
653 for (; node; node = node.next())
654 if (node.is("sound"))
655 ASSERT(processSound(node));
656 return true;
659 bool Entity::processSound(const XMLNode node)
661 const std::string name = node.attr("name");
662 const std::string filename = node.content();
663 if (name.empty()) {
664 Log::err(descriptor, "<sound> name attribute is empty");
665 return false;
666 } else if (filename.empty()) {
667 Log::err(descriptor, "<sound></sound> is empty");
668 return false;
671 Resourcer* rc = Resourcer::instance();
672 SampleRef s = rc->getSample(filename);
673 if (s)
674 sounds[name] = s;
675 return true;
678 bool Entity::processScripts(XMLNode node)
680 for (; node; node = node.next())
681 if (node.is("script"))
682 ASSERT(processScript(node));
683 return true;
686 bool Entity::processScript(const XMLNode node)
688 const std::string trigger = node.attr("trigger");
689 const std::string filename = node.content();
690 if (trigger.empty()) {
691 Log::err(descriptor, "<script> trigger attribute is empty");
692 return false;
693 } else if (filename.empty()) {
694 Log::err(descriptor, "<script></script> is empty");
695 return false;
698 ScriptInst script(filename);
699 if (!script.validate(descriptor))
700 return false;
702 if (!addScript(trigger, script)) {
703 Log::err(descriptor,
704 "unrecognized script trigger: " + trigger);
705 return false;
708 return true;
711 bool Entity::addScript(const std::string& trigger, ScriptInst& script)
713 if (boost::iequals(trigger, "on_update")) {
714 updateHooks.push_back(script);
715 return true;
717 if (boost::equals(trigger, "on_tile_entry")) {
718 tileEntryHooks.push_back(script);
719 return true;
721 if (boost::iequals(trigger, "on_tile_exit")) {
722 tileExitHooks.push_back(script);
723 return true;
725 return false;
729 void exportEntity()
731 using namespace boost::python;
733 class_<Entity>("Entity")
734 .def("init", &Entity::init)
735 .add_property("frozen", &Entity::getFrozen, &Entity::setFrozen)
736 .add_property("phase", &Entity::getPhase, &Entity::setPhase)
737 .add_property("area",
738 make_function(&Entity::getArea,
739 return_value_policy<reference_existing_object>()),
740 &Entity::setArea)
741 .add_property("tile", make_function(
742 static_cast<Tile& (Entity::*) ()> (&Entity::getTile),
743 return_value_policy<reference_existing_object>()))
744 .add_property("coords", &Entity::getTileCoords_vi)
745 .add_property("speed", &Entity::getSpeed, &Entity::setSpeed)
746 .add_property("moving", &Entity::isMoving)
747 .add_property("exempt", &Entity::exemptManip)
748 .def("set_coords",
749 static_cast<void (Entity::*) (int,int,double)>
750 (&Entity::setTileCoords))
751 .def("move", static_cast<void (Entity::*) (int,int)>
752 (&Entity::moveByTile))
753 .def("teleport", static_cast<void (Entity::*) (int,int)>
754 (&Entity::setTileCoords))
755 .def("add_on_update_listener", &Entity::addOnUpdateListener)
756 .def("move",
757 static_cast<void (Entity::*) (int,int)>
758 (&Entity::moveByTile));