Fixes for windows.
[Tsunagari.git] / src / entity.cpp
blobdfba34f3d25293d8cd820a1e87cdb38e18a9bb43
1 /******************************
2 ** Tsunagari Tile Engine **
3 ** entity.cpp **
4 ** Copyright 2011 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 "resourcer.h"
19 #include "window.h"
20 #include "xml.h"
22 #define ASSERT(x) if (!(x)) return false
24 static std::string facings[][3] = {
25 {"up-left", "up", "up-right"},
26 {"left", "", "right"},
27 {"down-left", "down", "down-right"},
31 Entity::Entity(Resourcer* rc, Area* area, ClientValues* conf)
32 : rc(rc),
33 redraw(true),
34 speedMul(1.0),
35 moving(false),
36 area(NULL),
37 r(0.0, 0.0, 0.0),
38 conf(conf)
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 phase = &phases.begin()->second;
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, 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 // Save state of partial pixel travel in double.
157 r.x += dx * traveled;
158 r.y += dy * traveled;
162 void Entity::updateNoTile(unsigned long)
164 // TODO
167 bool Entity::setPhase(const std::string& name)
169 boost::unordered_map<std::string, Animation>::iterator it;
170 it = phases.find(name);
171 if (it != phases.end()) {
172 Animation* newPhase = &it->second;
173 if (phase != newPhase) {
174 phase = newPhase;
175 redraw = true;
176 return true;
179 return false;
182 rcoord Entity::getPixelCoord() const
184 return r;
187 icoord Entity::getTileCoords() const
189 ivec2 tileDim = area->getTileDimensions();
190 return icoord(
191 (int)r.x / tileDim.x,
192 (int)r.y / tileDim.y,
193 area->depthIndex(r.z)
197 void Entity::setTileCoords(icoord coords)
199 // FIXME: security: bounds check
200 redraw = true;
201 const ivec2 tileDim = area->getTileDimensions();
202 r = rcoord(
203 coords.x * tileDim.x,
204 coords.y * tileDim.y,
205 coords.z
209 void Entity::moveByTile(icoord delta)
211 if (moving)
212 return;
213 calculateFacing(delta.x, delta.y);
214 setPhase(facing);
216 std::vector<icoord> tiles = frontTiles();
217 BOOST_FOREACH(const icoord& tile, tiles) {
218 if (canMove(tile)) {
219 preMove();
220 return;
225 void Entity::setArea(Area* a)
227 area = a;
228 calcDoff();
229 setSpeed(speedMul); // Calculate new speed based on tile size.
232 void Entity::gotoRandomTile()
234 const icoord map = area->getDimensions();
235 icoord pos;
236 Tile* tile;
237 do {
238 pos = icoord(rand() % map.x, rand() % map.y, 0);
239 tile = &area->getTile(pos);
240 } while (tile->hasFlag(nowalk));
241 setTileCoords(pos);
244 void Entity::setSpeed(double multiplier)
246 speedMul = multiplier;
247 if (area) {
248 double tilesPerSecond = area->getTileDimensions().x / 1000.0;
249 speed = baseSpeed * speedMul * tilesPerSecond;
253 std::vector<icoord> Entity::frontTiles() const
255 std::vector<icoord> tiles;
256 icoord normal = getTileCoords();
257 normal += icoord(faceX, faceY, 0);
258 tiles.push_back(normal);
260 // If we are on a tile with the layermod property, we have access to
261 // tiles on the new layer, too.
262 const boost::optional<int> layermod = getTile().layermod;
263 if (layermod) {
264 icoord mod = normal;
265 mod.z = area->depthIndex(*layermod);
266 tiles.push_back(mod);
268 return tiles;
271 void Entity::calcDoff()
273 // X-axis is centered on tile.
274 doff.x = (area->getTileDimensions().x - imgw) / 2;
275 // Y-axis is aligned with bottom of tile.
276 doff.y = area->getTileDimensions().y - imgh - 1;
279 Tile& Entity::getTile() const
281 return area->getTile(getTileCoords());
284 SampleRef Entity::getSound(const std::string& name)
286 boost::unordered_map<std::string, SampleRef>::iterator it;
288 it = sounds.find(name);
289 if (it != sounds.end())
290 return it->second;
291 else
292 return SampleRef();
295 void Entity::calculateFacing(int x, int y)
297 if (x < 0)
298 faceX = -1;
299 else if (x == 0)
300 faceX = 0;
301 else
302 faceX = 1;
304 if (y < 0)
305 faceY = -1;
306 else if (y == 0)
307 faceY = 0;
308 else
309 faceY = 1;
311 facing = facings[faceY+1][faceX+1];
314 bool Entity::canMove(icoord dest)
316 if (!area->tileExists(dest))
317 // The tile is off the map.
318 return false;
319 ivec2 tileDim = area->getTileDimensions();
320 destCoord = rcoord(
321 dest.x * tileDim.x,
322 dest.y * tileDim.y,
323 area->indexDepth(dest.z)
325 destTile = &area->getTile(dest);
326 return !destTile->hasFlag(nowalk);
329 void Entity::preMove()
331 fromCoord = r;
332 fromTile = &getTile();
333 moving = true;
335 // Set z right away so that we're on-level with the square we're entering.
336 r.z = destCoord.z;
338 // Start moving animation.
339 switch (conf->moveMode) {
340 case TURN:
341 break;
342 case TILE:
343 case NOTILE:
344 setPhase("moving " + facing);
345 break;
348 // Process triggers.
349 tileExitScript();
351 if (conf->moveMode == TURN) {
352 // Movement is instantaneous.
353 r = destCoord;
354 postMove();
358 void Entity::postMove()
360 moving = false;
362 // Stop moving animation.
363 if (conf->moveMode != TURN)
364 setPhase(facing);
366 // Process triggers.
367 fromTile->onLeaveScripts(rc, this);
368 tileEntryScript();
369 destTile->onEnterScripts(rc, this);
371 // TODO: move teleportation here
373 * if (onDoor()) {
374 * leaveTile();
375 * moveArea(getDoor());
376 * postMoveScript();
377 * enterTile();
382 void Entity::tileExitScript()
384 const std::string& name = scripts["tileexit"];
385 if (name.size()) {
386 //std::string lines = rc->getText(name);
387 //pyExec(lines.c_str());
391 void Entity::tileEntryScript()
393 const std::string& name = scripts["tileentry"];
394 if (name.size()) {
395 //std::string lines = rc->getText(name);
396 //pyExec(lines.c_str());
402 * DESCRIPTOR CODE BELOW
405 bool Entity::processDescriptor()
407 XMLRef doc = rc->getXMLDoc(descriptor, "entity.dtd");
408 if (!doc)
409 return false;
410 const XMLNode root = doc->root(); // <entity>
411 if (!root)
412 return false;
414 for (XMLNode node = root.childrenNode(); node; node = node.next()) {
415 if (node.is("speed")) {
416 ASSERT(node.doubleContent(&baseSpeed));
417 setSpeed(speedMul); // Calculate speed from tile size.
418 } else if (node.is("sprite")) {
419 ASSERT(processSprite(node.childrenNode()));
420 } else if (node.is("sounds")) {
421 ASSERT(processSounds(node.childrenNode()));
422 } else if (node.is("scripts")) {
423 ASSERT(processScripts(node.childrenNode()));
426 return true;
429 bool Entity::processSprite(XMLNode node)
431 TiledImage tiles;
432 for (; node; node = node.next()) {
433 if (node.is("sheet")) {
434 std::string imageSheet = node.content();
435 ASSERT(node.intAttr("tilewidth", &imgw) &&
436 node.intAttr("tileheight", &imgh));
437 ASSERT(rc->getTiledImage(tiles, imageSheet,
438 imgw, imgh, false));
439 } else if (node.is("phases")) {
440 ASSERT(processPhases(node.childrenNode(), tiles));
443 return true;
446 bool Entity::processPhases(XMLNode node, const TiledImage& tiles)
448 for (; node; node = node.next())
449 if (node.is("phase"))
450 ASSERT(processPhase(node, tiles));
451 return true;
454 bool Entity::processPhase(const XMLNode node, const TiledImage& tiles)
456 /* Each phase requires a 'name'. Additionally,
457 * one of either 'pos' or 'speed' is needed.
458 * If speed is used, we have sub-elements. We
459 * can't have both pos and speed.
461 const std::string name = node.attr("name");
462 if (name.empty()) {
463 Log::err(descriptor, "<phase> name attribute is empty");
464 return false;
467 const std::string posStr = node.attr("pos");
468 const std::string speedStr = node.attr("speed");
470 if (posStr.size() && speedStr.size()) {
471 Log::err(descriptor, "pos and speed attributes in "
472 "phase element are mutually exclusive");
473 return false;
474 } else if (posStr.empty() && speedStr.empty()) {
475 Log::err(descriptor, "must have pos or speed attribute "
476 "in phase element");
477 return false;
480 if (posStr.size()) {
481 int pos;
482 ASSERT(node.intAttr("pos", &pos));
483 if (pos < 0 || (int)tiles.size() < pos) {
484 Log::err(descriptor,
485 "<phase></phase> index out of bounds");
486 return false;
488 phases[name].addFrame(tiles[pos]);
490 else {
491 int speed;
492 ASSERT(node.intAttr("speed", &speed));
494 int len = (int)(1000.0/speed);
495 phases[name].setFrameLen(len);
496 ASSERT(processMembers(node.childrenNode(),
497 phases[name], tiles));
500 return true;
503 bool Entity::processMembers(XMLNode node, Animation& anim,
504 const TiledImage& tiles)
506 for (; node; node = node.next())
507 if (node.is("member"))
508 ASSERT(processMember(node, anim, tiles));
509 return true;
512 bool Entity::processMember(const XMLNode node, Animation& anim,
513 const TiledImage& tiles)
515 int pos;
516 ASSERT(node.intAttr("pos", &pos));
517 if (pos < 0 || (int)tiles.size() < pos) {
518 Log::err(descriptor, "<member></member> index out of bounds");
519 return false;
521 anim.addFrame(tiles[pos]);
522 return true;
525 bool Entity::processSounds(XMLNode node)
527 for (; node; node = node.next())
528 if (node.is("sound"))
529 ASSERT(processSound(node));
530 return true;
533 bool Entity::processSound(const XMLNode node)
535 const std::string name = node.attr("name");
536 const std::string filename = node.content();
537 if (name.empty()) {
538 Log::err(descriptor, "<sound> name attribute is empty");
539 return false;
540 } else if (filename.empty()) {
541 Log::err(descriptor, "<sound></sound> is empty");
542 return false;
545 SampleRef s = rc->getSample(filename);
546 if (s)
547 sounds[name] = s;
548 return true;
551 bool Entity::processScripts(XMLNode node)
553 for (; node; node = node.next())
554 if (node.is("script"))
555 ASSERT(processScript(node));
556 return true;
559 bool Entity::processScript(const XMLNode node)
561 const std::string trigger = node.attr("trigger");
562 const std::string filename = node.content();
563 if (trigger.empty()) {
564 Log::err(descriptor, "<script> trigger attribute is empty");
565 return false;
566 } else if (filename.empty()) {
567 Log::err(descriptor, "<script></script> is empty");
568 return false;
571 if (rc->resourceExists(filename)) {
572 scripts[trigger] = filename;
573 return true;
575 else {
576 Log::err(descriptor, std::string("script not found: ") + filename);
577 return false;