uppercase script parameters
[Tsunagari.git] / src / entity.cpp
blob2e1d735fd9b03c9c37c7d45495eb29745457e46a
1 /*********************************
2 ** Tsunagari Tile Engine **
3 ** entity.cpp **
4 ** Copyright 2011-2012 OmegaSDG **
5 *********************************/
7 #include <math.h>
9 #include <boost/foreach.hpp>
10 #include <Gosu/Image.hpp>
11 #include <Gosu/Math.hpp>
12 #include <Gosu/Timing.hpp>
14 #include "area.h"
15 #include "config.h"
16 #include "entity.h"
17 #include "log.h"
18 #include "python.h"
19 #include "resourcer.h"
20 #include "window.h"
21 #include "xml.h"
23 #define ASSERT(x) if (!(x)) return false
25 static std::string directions[][3] = {
26 {"up-left", "up", "up-right"},
27 {"left", "", "right"},
28 {"down-left", "down", "down-right"},
32 Entity::Entity(Area* area)
33 : redraw(true),
34 speedMul(1.0),
35 moving(false),
36 stillMoving(false),
37 area(NULL),
38 r(0.0, 0.0, 0.0)
40 if (area)
41 setArea(area);
44 Entity::~Entity()
48 bool Entity::init(const std::string& descriptor)
50 this->descriptor = descriptor;
51 if (!processDescriptor())
52 return false;
54 // Set an initial phase.
55 setPhase(directionStr(setFacing(ivec2(0, 1))));
56 return true;
59 void Entity::draw()
61 int millis = GameWindow::getWindow().time();
62 phase->updateFrame(millis);
63 phase->frame()->draw(doff.x + r.x, doff.y + r.y, r.z);
64 redraw = false;
67 bool Entity::needsRedraw() const
69 int millis = GameWindow::getWindow().time();
70 return redraw || phase->needsRedraw(millis);
74 static double angleFromXY(double x, double y)
76 double angle = 0.0;
78 // Moving at an angle
79 if (x != 0 && y != 0) {
80 angle = atan(y / x);
81 if (y < 0 && x < 0)
83 else if (y < 0 && x > 0)
84 angle += M_PI;
85 else if (y > 0 && x < 0)
86 angle += M_PI*2;
87 else if (y > 0 && x > 0)
88 angle += M_PI;
91 // Moving straight
92 else {
93 if (x < 0)
94 angle = 0;
95 else if (x > 0)
96 angle = M_PI;
97 else if (y < 0)
98 angle = M_PI_2;
99 else if (y > 0)
100 angle = 3*M_PI_2;
103 return angle;
106 void Entity::update(unsigned long dt)
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 // Save state of partial pixel travel in double.
158 r.x += dx * traveled;
159 r.y += dy * traveled;
163 void Entity::updateNoTile(unsigned long)
165 // TODO
168 const std::string Entity::getFacing() const
170 return directionStr(facing);
173 bool Entity::setPhase(const std::string& name)
175 AnimationMap::iterator it;
176 it = phases.find(name);
177 if (it != phases.end()) {
178 Animation* newPhase = &it->second;
179 if (phase != newPhase) {
180 int now = GameWindow::getWindow().time();
181 phase = newPhase;
182 phase->startOver(now);
183 redraw = true;
184 return true;
187 return false;
190 rcoord Entity::getPixelCoord() const
192 return r;
195 icoord Entity::getTileCoords_i() const
197 return area->virt2phys(r);
200 vicoord Entity::getTileCoords_vi() const
202 return area->virt2virt(r);
205 void Entity::setTileCoords(icoord phys)
207 if (!area->inBounds(phys))
208 return;
209 redraw = true;
210 r = area->phys2virt_r(phys);
213 void Entity::setTileCoords(vicoord virt)
215 if (!area->inBounds(virt))
216 return;
217 redraw = true;
218 r = area->virt2virt(virt);
221 void Entity::moveByTile(ivec2 delta)
223 if (moving)
224 return;
225 setFacing(delta);
227 std::vector<icoord> tiles = frontTiles();
228 BOOST_FOREACH(const icoord& tile, tiles) {
229 if (canMove(tile)) {
230 preMove();
231 return;
233 else
234 setPhase(directionStr(facing));
238 void Entity::setArea(Area* a)
240 area = a;
241 calcDoff();
242 setSpeed(speedMul); // Calculate new speed based on tile size.
245 void Entity::gotoRandomTile()
247 const icoord map = area->getDimensions();
248 icoord pos;
249 Tile* tile;
250 do {
251 pos = icoord(rand() % map.x, rand() % map.y, 0);
252 tile = &area->getTile(pos);
253 } while (tile->hasFlag(TILE_NOWALK));
254 setTileCoords(pos);
257 double Entity::getSpeed() const
259 return speedMul;
262 void Entity::setSpeed(double multiplier)
264 speedMul = multiplier;
265 if (area) {
266 double tilesPerSecond = area->getTileDimensions().x / 1000.0;
267 speed = baseSpeed * speedMul * tilesPerSecond;
271 Tile& Entity::getTile() const
273 return area->getTile(getTileCoords_i());
276 Tile& Entity::getTile()
278 return area->getTile(getTileCoords_i());
281 std::vector<icoord> Entity::frontTiles() const
283 std::vector<icoord> tiles;
284 icoord normal = getTileCoords_i();
285 normal += icoord(facing.x, facing.y, 0);
286 tiles.push_back(normal);
288 // If we are on a tile with the layermod property, we have access to
289 // tiles on the new layer, too.
290 const boost::optional<double> layermod = getTile().layermod;
291 if (layermod) {
292 icoord mod = area->virt2phys(
293 vicoord(normal.x, normal.y, *layermod));
294 tiles.push_back(mod);
296 return tiles;
299 void Entity::calcDoff()
301 // X-axis is centered on tile.
302 doff.x = (area->getTileDimensions().x - imgw) / 2;
303 // Y-axis is aligned with bottom of tile.
304 doff.y = area->getTileDimensions().y - imgh - 1;
307 SampleRef Entity::getSound(const std::string& name) const
309 SampleMap::const_iterator it;
310 it = sounds.find(name);
311 if (it != sounds.end())
312 return it->second;
313 else
314 return SampleRef();
317 ivec2 Entity::setFacing(ivec2 facing)
319 this->facing = ivec2(
320 Gosu::clamp(facing.x, -1, 1),
321 Gosu::clamp(facing.y, -1, 1)
323 return this->facing;
326 const std::string& Entity::directionStr(ivec2 facing) const
328 return directions[facing.y+1][facing.x+1];
331 bool Entity::canMove(icoord dest)
333 bool can = false, inBounds;
334 icoord delta = dest;
335 delta -= getTileCoords_i();
336 can = can || (inBounds = area->inBounds(dest));
337 can = can || (delta.z == 0 && getTile().exitAt(delta.x, delta.y));
338 if (!can)
339 // The tile is off the map.
340 return false;
341 destCoord = area->phys2virt_r(dest);
342 if (inBounds) {
343 destTile = &area->getTile(dest);
344 return !destTile->hasFlag(TILE_NOWALK);
346 else {
347 destTile = NULL;
348 return true;
352 void Entity::preMove()
354 fromCoord = r;
355 fromTile = &getTile();
356 moving = true;
358 // Set z right away so that we're on-level with the square we're
359 // entering.
360 r.z = destCoord.z;
362 // Start moving animation.
363 switch (conf.moveMode) {
364 case TURN:
365 break;
366 case TILE:
367 case NOTILE:
368 setPhase("moving " + getFacing());
369 break;
372 // Process triggers.
373 tileExitScript();
374 fromTile->onLeaveScripts(this);
376 if (conf.moveMode == TURN) {
377 // Movement is instantaneous.
378 r = destCoord;
379 postMove();
383 void Entity::postMove()
385 moving = false;
387 // Stop moving animation.
388 if (conf.moveMode != TURN && !stillMoving)
389 setPhase(getFacing());
391 // Process triggers.
392 if (destTile) {
393 destTile->onEnterScripts(this);
394 tileEntryScript();
397 // TODO: move teleportation here
399 * if (onExit()) {
400 * leaveTile();
401 * moveArea(getExit());
402 * postMoveScript();
403 * enterTile();
408 void Entity::tileExitScript()
410 Resourcer* rc = Resourcer::instance();
411 const std::string& name = scripts["tileexit"];
412 if (name.size()) {
413 pythonSetGlobal("Entity", this);
414 rc->runPythonScript(name);
418 void Entity::tileEntryScript()
420 Resourcer* rc = Resourcer::instance();
421 const std::string& name = scripts["tileentry"];
422 if (name.size()) {
423 pythonSetGlobal("Entity", this);
424 rc->runPythonScript(name);
430 * DESCRIPTOR CODE BELOW
433 bool Entity::processDescriptor()
435 Resourcer* rc = Resourcer::instance();
436 XMLRef doc = rc->getXMLDoc(descriptor, "entity.dtd");
437 if (!doc)
438 return false;
439 const XMLNode root = doc->root(); // <entity>
440 if (!root)
441 return false;
443 for (XMLNode node = root.childrenNode(); node; node = node.next()) {
444 if (node.is("speed")) {
445 ASSERT(node.doubleContent(&baseSpeed));
446 setSpeed(speedMul); // Calculate speed from tile size.
447 } else if (node.is("sprite")) {
448 ASSERT(processSprite(node.childrenNode()));
449 } else if (node.is("sounds")) {
450 ASSERT(processSounds(node.childrenNode()));
451 } else if (node.is("scripts")) {
452 ASSERT(processScripts(node.childrenNode()));
455 return true;
458 bool Entity::processSprite(XMLNode node)
460 Resourcer* rc = Resourcer::instance();
461 TiledImage tiles;
462 for (; node; node = node.next()) {
463 if (node.is("sheet")) {
464 std::string imageSheet = node.content();
465 ASSERT(node.intAttr("tilewidth", &imgw) &&
466 node.intAttr("tileheight", &imgh));
467 ASSERT(rc->getTiledImage(tiles, imageSheet,
468 imgw, imgh, false));
469 } else if (node.is("phases")) {
470 ASSERT(processPhases(node.childrenNode(), tiles));
473 return true;
476 bool Entity::processPhases(XMLNode node, const TiledImage& tiles)
478 for (; node; node = node.next())
479 if (node.is("phase"))
480 ASSERT(processPhase(node, tiles));
481 return true;
484 bool Entity::processPhase(const XMLNode node, const TiledImage& tiles)
486 /* Each phase requires a 'name'. Additionally,
487 * one of either 'pos' or 'speed' is needed.
488 * If speed is used, we have sub-elements. We
489 * can't have both pos and speed.
491 const std::string name = node.attr("name");
492 if (name.empty()) {
493 Log::err(descriptor, "<phase> name attribute is empty");
494 return false;
497 const std::string posStr = node.attr("pos");
498 const std::string speedStr = node.attr("speed");
500 if (posStr.size() && speedStr.size()) {
501 Log::err(descriptor, "pos and speed attributes in "
502 "phase element are mutually exclusive");
503 return false;
504 } else if (posStr.empty() && speedStr.empty()) {
505 Log::err(descriptor, "must have pos or speed attribute "
506 "in phase element");
507 return false;
510 if (posStr.size()) {
511 int pos;
512 ASSERT(node.intAttr("pos", &pos));
513 if (pos < 0 || (int)tiles.size() < pos) {
514 Log::err(descriptor,
515 "<phase></phase> index out of bounds");
516 return false;
518 phases[name].addFrame(tiles[pos]);
520 else {
521 int speed;
522 ASSERT(node.intAttr("speed", &speed));
524 int len = (int)(1000.0/speed);
525 phases[name].setFrameLen(len);
526 ASSERT(processMembers(node.childrenNode(),
527 phases[name], tiles));
530 return true;
533 bool Entity::processMembers(XMLNode node, Animation& anim,
534 const TiledImage& tiles)
536 for (; node; node = node.next())
537 if (node.is("member"))
538 ASSERT(processMember(node, anim, tiles));
539 return true;
542 bool Entity::processMember(const XMLNode node, Animation& anim,
543 const TiledImage& tiles)
545 int pos;
546 ASSERT(node.intAttr("pos", &pos));
547 if (pos < 0 || (int)tiles.size() < pos) {
548 Log::err(descriptor, "<member></member> index out of bounds");
549 return false;
551 anim.addFrame(tiles[pos]);
552 return true;
555 bool Entity::processSounds(XMLNode node)
557 for (; node; node = node.next())
558 if (node.is("sound"))
559 ASSERT(processSound(node));
560 return true;
563 bool Entity::processSound(const XMLNode node)
565 const std::string name = node.attr("name");
566 const std::string filename = node.content();
567 if (name.empty()) {
568 Log::err(descriptor, "<sound> name attribute is empty");
569 return false;
570 } else if (filename.empty()) {
571 Log::err(descriptor, "<sound></sound> is empty");
572 return false;
575 Resourcer* rc = Resourcer::instance();
576 SampleRef s = rc->getSample(filename);
577 if (s)
578 sounds[name] = s;
579 return true;
582 bool Entity::processScripts(XMLNode node)
584 for (; node; node = node.next())
585 if (node.is("script"))
586 ASSERT(processScript(node));
587 return true;
590 bool Entity::processScript(const XMLNode node)
592 const std::string trigger = node.attr("trigger");
593 const std::string filename = node.content();
594 if (trigger.empty()) {
595 Log::err(descriptor, "<script> trigger attribute is empty");
596 return false;
597 } else if (filename.empty()) {
598 Log::err(descriptor, "<script></script> is empty");
599 return false;
602 Resourcer* rc = Resourcer::instance();
603 if (rc->resourceExists(filename)) {
604 scripts[trigger] = filename;
605 return true;
607 else {
608 Log::err(descriptor,
609 std::string("script not found: ") + filename);
610 return false;
615 void exportEntity()
617 boost::python::class_<Entity>("Entity", boost::python::no_init)
618 .add_property("animation",
619 &Entity::getFacing, &Entity::setPhase)
620 .add_property("tile",
621 make_function(
622 static_cast<Tile& (Entity::*) ()> (&Entity::getTile),
623 boost::python::return_value_policy<
624 boost::python::reference_existing_object
628 .add_property("coords", &Entity::getTileCoords_vi)
629 .add_property("speed", &Entity::getSpeed, &Entity::setSpeed)
630 .def("goto_random_tile", &Entity::gotoRandomTile)