factored generic Animation class out of Area::TileType
[Tsunagari.git] / src / area.cpp
blob07942e05d94fe5fd84125b9c3f411e719bd455dc
1 /******************************
2 ** Tsunagari Tile Engine **
3 ** area.cpp **
4 ** Copyright 2011 OmegaSDG **
5 ******************************/
7 #include <math.h>
9 #include <boost/foreach.hpp>
10 #include <boost/shared_ptr.hpp>
11 #include <Gosu/Graphics.hpp>
12 #include <Gosu/Image.hpp>
13 #include <Gosu/Math.hpp>
14 #include <Gosu/Timing.hpp>
16 #include "area.h"
17 #include "common.h"
18 #include "entity.h"
19 #include "log.h"
20 #include "resourcer.h"
21 #include "window.h"
22 #include "world.h"
24 /* NOTE: In the TMX map format used by Tiled, tileset tiles start counting
25 their Y-positions from 0, while layer tiles start counting from 1. I
26 can't imagine why the author did this, but we have to take it into
27 account.
30 Area::Area(Resourcer* rc,
31 World* world,
32 Player* player,
33 const std::string& descriptor)
34 : rc(rc), world(world), player(player), descriptor(descriptor),
35 onIntro(false)
37 dim.x = dim.y = dim.z = 0;
40 Area::~Area()
42 if (musicInst && musicInst->playing())
43 musicInst->stop();
46 bool Area::init()
48 if (!processDescriptor())
49 return false;
51 if (introMusic) {
52 musicInst.reset(introMusic->play(1, 1, false));
53 onIntro = true;
55 else if (mainMusic) {
56 musicInst.reset(mainMusic->play(1, 1, true));
58 return true;
61 void Area::buttonDown(const Gosu::Button btn)
63 if (btn == Gosu::kbRight)
64 player->startMovement(coord(1, 0, 0));
65 else if (btn == Gosu::kbLeft)
66 player->startMovement(coord(-1, 0, 0));
67 else if (btn == Gosu::kbUp)
68 player->startMovement(coord(0, -1, 0));
69 else if (btn == Gosu::kbDown)
70 player->startMovement(coord(0, 1, 0));
73 void Area::buttonUp(const Gosu::Button btn)
75 if (btn == Gosu::kbRight)
76 player->stopMovement(coord(1, 0, 0));
77 else if (btn == Gosu::kbLeft)
78 player->stopMovement(coord(-1, 0, 0));
79 else if (btn == Gosu::kbUp)
80 player->stopMovement(coord(0, -1, 0));
81 else if (btn == Gosu::kbDown)
82 player->stopMovement(coord(0, 1, 0));
85 void Area::draw()
87 Gosu::Graphics& graphics = GameWindow::getWindow().graphics();
88 const Gosu::Transform trans = viewportTransform();
89 graphics.pushTransform(trans);
91 drawTiles();
92 drawEntities();
94 graphics.popTransform();
97 void Area::drawTiles()
99 // Calculate frame to show for each type of tile
100 int millis = (int)Gosu::milliseconds();
101 BOOST_FOREACH(TileSet& set, tilesets)
102 BOOST_FOREACH(TileType& type, set.tileTypes)
103 type.anim.updateFrame(millis);
105 // Render
106 for (unsigned z = 0; z != map.size(); z++) {
107 const grid_t& grid = map[z];
108 for (unsigned y = 0; y != grid.size(); y++) {
109 const row_t& row = grid[y];
110 for (unsigned x = 0; x != row.size(); x++) {
111 const Tile& tile = row[x];
112 const TileType* type = tile.type;
113 const Gosu::Image* img = type->anim.image();
114 img->draw(x*img->width(), y*img->height(), 0);
120 void Area::drawEntities()
122 player->draw();
125 bool Area::needsRedraw() const
127 if (player->needsRedraw())
128 return true;
130 // Do any onscreen tile types need to update their animations?
131 int millis = (int)Gosu::milliseconds();
132 BOOST_FOREACH(const TileSet& set, tilesets)
133 BOOST_FOREACH(const TileType& type, set.tileTypes)
134 if (type.anim.needsUpdate(millis) &&
135 tileTypeOnScreen(type))
136 return true;
137 return false;
140 void Area::update(unsigned long dt)
142 if (onIntro && !musicInst->playing()) {
143 onIntro = false;
144 musicInst.reset(mainMusic->play(1, 1, true));
146 player->update(dt);
149 coord_t Area::getDimensions() const
151 return dim;
154 coord_t Area::getTileDimensions() const
156 return tilesets[0].tileDim; // XXX only considers first tileset
159 const Area::Tile& Area::getTile(coord_t c) const
161 return map[c.z][c.y][c.x];
164 Area::Tile& Area::getTile(coord_t c)
166 return map[c.z][c.y][c.x];
169 static double center(double w, double g, double p)
171 return w>g ? (w-g)/2.0 : Gosu::boundBy(w/2.0-p, w-g, 0.0);
174 const coord_t Area::viewportOffset() const
176 const Gosu::Graphics& graphics = GameWindow::getWindow().graphics();
177 const double tileWidth = (double)tilesets[0].tileDim.x;
178 const double tileHeight = (double)tilesets[0].tileDim.y;
179 const double windowWidth = (double)graphics.width() / tileWidth;
180 const double windowHeight = (double)graphics.height() / tileHeight;
181 const double gridWidth = (double)dim.x;
182 const double gridHeight = (double)dim.y;
183 const double playerX = (double)player->getCoordsByPixel().x /
184 tileWidth + 0.5;
185 const double playerY = (double)player->getCoordsByPixel().y /
186 tileHeight + 0.5;
188 coord_t c;
189 c.x = (long)(center(windowWidth, gridWidth, playerX) * tileWidth);
190 c.y = (long)(center(windowHeight, gridHeight, playerY) * tileHeight);
191 c.z = 0;
193 return c;
196 const Gosu::Transform Area::viewportTransform() const
198 const coord_t c = viewportOffset();
199 return Gosu::translate((double)c.x, (double)c.y);
202 cube_t Area::visibleTiles() const
204 const Gosu::Graphics& graphics = GameWindow::getWindow().graphics();
205 const long tileWidth = tilesets[0].tileDim.x;
206 const long tileHeight = tilesets[0].tileDim.y;
207 const int windowWidth = graphics.width();
208 const int windowHeight = graphics.height();
209 const coord_t off = viewportOffset();
211 const long x2 = (long)ceil((double)(windowWidth - off.x) /
212 (double)tileWidth);
213 const long y2 = (long)ceil((double)(windowHeight - off.y) /
214 (double)tileHeight);
215 return cube(-off.x / tileWidth, -off.y / tileHeight, 0,
216 x2, y2, 1);
219 bool Area::tileTypeOnScreen(const Area::TileType& search) const
221 const cube_t tiles = visibleTiles();
222 for (long z = tiles.z1; z != tiles.z2; z++) {
223 for (long y = tiles.y1; y != tiles.y2; y++) {
224 for (long x = tiles.x1; x != tiles.x2; x++) {
225 const Tile& tile = map[z][y][x];
226 const TileType* type = tile.type;
227 if (type == &search)
228 return true;
232 return false;
235 bool Area::processDescriptor()
237 XMLDocRef doc = rc->getXMLDoc(descriptor, "dtd/area.dtd");
238 if (!doc)
239 return false;
241 // Iterate and process children of <map>
242 xmlNode* root = xmlDocGetRootElement(doc.get()); // <map> element
244 xmlChar* width = xmlGetProp(root, BAD_CAST("width"));
245 xmlChar* height = xmlGetProp(root, BAD_CAST("height"));
246 dim.x = atol((const char*)width);
247 dim.y = atol((const char*)height);
249 xmlNode* child = root->xmlChildrenNode;
250 for (; child != NULL; child = child->next) {
251 if (!xmlStrncmp(child->name, BAD_CAST("properties"), 11)) {
252 if (!processMapProperties(child))
253 return false;
255 else if (!xmlStrncmp(child->name, BAD_CAST("tileset"), 8)) {
256 if (!processTileSet(child))
257 return false;
259 else if (!xmlStrncmp(child->name, BAD_CAST("layer"), 6)) {
260 if (!processLayer(child))
261 return false;
263 else if (!xmlStrncmp(child->name, BAD_CAST("objectgroup"), 12)) {
264 if (!processObjectGroup(child))
265 return false;
269 return true;
272 bool Area::processMapProperties(xmlNode* node)
276 <properties>
277 <property name="areaspec" value="1"/>
278 <property name="author" value="Michael D. Reiley"/>
279 <property name="name" value="Baby's First Area"/>
280 <property name="intro_music" value="intro.music"/>
281 <property name="main_music" value="wind.music"/>
282 <property name="onLoad" value="babysfirst_init()"/>
283 <property name="scripts" value="areainits.event,test.event"/>
284 </properties>
287 xmlNode* child = node->xmlChildrenNode;
288 for (; child != NULL; child = child->next) {
289 xmlChar* name = xmlGetProp(child, BAD_CAST("name"));
290 xmlChar* value = xmlGetProp(child, BAD_CAST("value"));
291 if (!xmlStrncmp(name, BAD_CAST("author"), 7))
292 author = (const char*)value;
293 else if (!xmlStrncmp(name, BAD_CAST("name"), 5))
294 this->name = (const char*)value;
295 else if (!xmlStrncmp(name, BAD_CAST("intro_music"), 12))
296 introMusic = rc->getSample((const char*)value);
297 else if (!xmlStrncmp(name, BAD_CAST("main_music"), 11))
298 mainMusic = rc->getSample((const char*)value);
299 else if (!xmlStrncmp(name, BAD_CAST("onLoad"), 7))
300 onLoadEvents = (const char*)value;
301 else if (!xmlStrncmp(name, BAD_CAST("scripts"), 8))
302 scripts = (const char*)value; // TODO split(), load
304 return true;
307 bool Area::processTileSet(xmlNode* node)
311 <tileset firstgid="1" name="tiles.sheet" tilewidth="64" tileheight="64">
312 <image source="tiles.sheet" width="256" height="256"/>
313 <tile id="14">
315 </tile>
316 </tileset>
318 TileSet ts;
319 xmlChar* width = xmlGetProp(node, BAD_CAST("tilewidth"));
320 xmlChar* height = xmlGetProp(node, BAD_CAST("tileheight"));
321 long x = ts.tileDim.x = atol((const char*)width);
322 long y = ts.tileDim.y = atol((const char*)height);
324 xmlNode* child = node->xmlChildrenNode;
325 for (; child != NULL; child = child->next) {
326 if (!xmlStrncmp(child->name, BAD_CAST("tile"), 5)) {
327 xmlChar* idstr = xmlGetProp(child, BAD_CAST("id"));
328 unsigned id = (unsigned)atoi((const char*)idstr);
330 // Undeclared TileTypes have default properties.
331 while (ts.tileTypes.size() != id) {
332 TileType tt = defaultTileType(ts);
333 ts.tileTypes.push_back(tt);
336 // Handle explicit TileType
337 if (!processTileType(child, ts))
338 return false;
340 else if (!xmlStrncmp(child->name, BAD_CAST("image"), 6)) {
341 const char* source = (const char*)xmlGetProp(child,
342 BAD_CAST("source"));
343 rc->getTiledImage(ts.tiles, source,
344 (unsigned)x, (unsigned)y, true);
348 while (ts.tiles.size()) {
349 TileType tt = defaultTileType(ts);
350 ts.tileTypes.push_back(tt);
353 tilesets.push_back(ts);
354 return true;
357 Area::TileType Area::defaultTileType(TileSet& set)
359 TileType type;
360 type.anim.addFrame(set.tiles.front());
361 set.tiles.pop_front();
362 return type;
365 bool Area::processTileType(xmlNode* node, TileSet& set)
369 <tile id="8">
370 <properties>
371 <property name="flags" value="nowalk"/>
372 <property name="onEnter" value="skid();speed(2)"/>
373 <property name="onLeave" value="undo()"/>
374 </properties>
375 </tile>
376 <tile id="14">
377 <properties>
378 <property name="animated" value="1"/>
379 <property name="size" value="2"/>
380 <property name="speed" value="2"/>
381 </properties>
382 </tile>
385 // Initialize a default TileType, we'll build on that.
386 TileType type = defaultTileType(set);
388 xmlChar* idstr = xmlGetProp(node, BAD_CAST("id"));
389 unsigned id = (unsigned)atoi((const char*)idstr); // atoi
390 long expectedId = set.tileTypes.size();
391 if (id != expectedId) {
392 Log::err(descriptor, std::string("expected TileType id ") +
393 itostr(expectedId) + ", but got " +
394 itostr(id));
395 return false;
398 xmlNode* child = node->xmlChildrenNode; // <properties>
399 child = child->xmlChildrenNode; // <property>
400 for (; child != NULL; child = child->next) {
401 xmlChar* name = xmlGetProp(child, BAD_CAST("name"));
402 xmlChar* value = xmlGetProp(child, BAD_CAST("value"));
403 if (!xmlStrncmp(name, BAD_CAST("flags"), 6)) {
404 type.flags = splitTileFlags((const char*)value);
406 else if (!xmlStrncmp(name, BAD_CAST("onEnter"), 8)) {
407 // TODO events
409 else if (!xmlStrncmp(name, BAD_CAST("onLeave"), 8)) {
410 // TODO events
412 else if (!xmlStrncmp(name, BAD_CAST("animated"), 9)) {
413 // XXX still needed?
414 // type.animated = parseBool((const char*)value);
416 else if (!xmlStrncmp(name, BAD_CAST("size"), 5)) {
417 int size = atoi((const char*)value); // atoi
419 // Add size-1 more frames to our animation.
420 // We already have one from defaultTileType.
421 for (int i = 1; i < size; i++) {
422 if (set.tiles.empty()) {
423 Log::err(descriptor, "ran out of tiles"
424 "/frames for animated tile");
425 return false;
427 type.anim.addFrame(set.tiles.front());
428 set.tiles.pop_front();
431 else if (!xmlStrncmp(name, BAD_CAST("speed"), 6)) {
432 int len = (int)(1000.0/atof((const char*)value));
433 type.anim.setFrameLen(len);
437 set.tileTypes.push_back(type);
438 return true;
441 bool Area::processLayer(xmlNode* node)
445 <layer name="Tiles0" width="5" height="5">
446 <properties>
448 </properties>
449 <data>
450 <tile gid="9"/>
451 <tile gid="9"/>
452 <tile gid="9"/>
454 <tile gid="3"/>
455 <tile gid="9"/>
456 <tile gid="9"/>
457 </data>
458 </layer>
461 xmlChar* width = xmlGetProp(node, BAD_CAST("width"));
462 xmlChar* height = xmlGetProp(node, BAD_CAST("height"));
463 int x = atoi((const char*)width);
464 int y = atoi((const char*)height);
466 if (dim.x != x || dim.y != y) {
467 Log::err(descriptor, "layer x,y size != map x,y size");
468 return false;
471 xmlNode* child = node->xmlChildrenNode;
472 for (; child != NULL; child = child->next) {
473 if (!xmlStrncmp(child->name, BAD_CAST("properties"), 11)) {
474 if (!processLayerProperties(child))
475 return false;
477 else if (!xmlStrncmp(child->name, BAD_CAST("data"), 5)) {
478 if (!processLayerData(child))
479 return false;
482 return true;
485 bool Area::processLayerProperties(xmlNode* node)
489 <properties>
490 <property name="layer" value="0"/>
491 </properties>
494 xmlNode* child = node->xmlChildrenNode;
495 for (; child != NULL; child = child->next) {
496 xmlChar* name = xmlGetProp(child, BAD_CAST("name"));
497 xmlChar* value = xmlGetProp(child, BAD_CAST("value"));
498 if (!xmlStrncmp(name, BAD_CAST("layer"), 6)) {
499 int depth = atoi((const char*)value);
500 if (depth != dim.z) {
501 Log::err(descriptor, "invalid layer depth");
502 return false;
507 return true;
510 bool Area::processLayerData(xmlNode* node)
514 <data>
515 <tile gid="9"/>
516 <tile gid="9"/>
517 <tile gid="9"/>
519 <tile gid="3"/>
520 <tile gid="9"/>
521 <tile gid="9"/>
522 </data>
525 row_t row;
526 grid_t grid;
528 row.reserve(dim.x);
529 grid.reserve(dim.y);
531 xmlNode* child = node->xmlChildrenNode;
532 for (int i = 1; child != NULL; i++, child = child->next) {
533 if (!xmlStrncmp(child->name, BAD_CAST("tile"), 5)) {
534 xmlChar* gidStr = xmlGetProp(child, BAD_CAST("gid"));
535 unsigned gid = (unsigned)atoi((const char*)gidStr)-1;
537 // XXX can only access first tileset
538 TileType* type = &tilesets[0].tileTypes[gid];
540 Tile t;
541 t.type = type;
542 t.flags = 0x0;
543 type->allOfType.push_back(&t);
544 row.push_back(t);
545 if (row.size() % dim.x == 0) {
546 grid.push_back(row);
547 row.clear();
548 row.reserve(dim.x);
553 map.push_back(grid);
554 dim.z++;
555 return true;
558 bool Area::processObjectGroup(xmlNode* node)
562 <objectgroup name="Prop0" width="5" height="5">
563 <properties>
564 <property name="layer" value="0"/>
565 </properties>
566 <object name="tile2" type="Tile" gid="7" x="64" y="320">
567 <properties>
568 <property name="onEnter" value="speed(0.5)"/>
569 <property name="onLeave" value="undo()"/>
570 <property name="door" value="grassfield.area,1,1,0"/>
571 <property name="flags" value="npc_nowalk"/>
572 </properties>
573 </object>
574 </objectgroup>
577 xmlChar* width = xmlGetProp(node, BAD_CAST("width"));
578 xmlChar* height = xmlGetProp(node, BAD_CAST("height"));
579 int x = atoi((const char*)width);
580 int y = atoi((const char*)height);
582 int zpos = -1;
584 if (dim.x != x || dim.y != y) {
585 Log::err(descriptor, "objectgroup x,y size != map x,y size");
586 return false;
589 xmlNode* child = node->xmlChildrenNode;
590 for (; child != NULL; child = child->next) {
591 if (!xmlStrncmp(child->name, BAD_CAST("properties"), 11)) {
592 if (!processObjectGroupProperties(child, &zpos))
593 return false;
595 else if (!xmlStrncmp(child->name, BAD_CAST("object"), 7)) {
596 if (zpos == -1 || !processObject(child, zpos))
597 return false;
601 return true;
604 bool Area::processObjectGroupProperties(xmlNode* node, int* zpos)
608 <properties>
609 <property name="layer" value="0"/>
610 </properties>
613 xmlNode* child = node->xmlChildrenNode;
614 for (; child != NULL; child = child->next) {
615 xmlChar* name = xmlGetProp(child, BAD_CAST("name"));
616 xmlChar* value = xmlGetProp(child, BAD_CAST("value"));
617 if (!xmlStrncmp(name, BAD_CAST("layer"), 6)) {
618 int layer = atoi((const char*)value);
619 if (0 < layer || layer >= (int)dim.z) {
620 Log::err(descriptor,
621 "objectgroup must correspond with layer"
623 return false;
625 *zpos = layer;
628 return true;
631 bool Area::processObject(xmlNode* node, int zpos)
635 <object name="tile2" type="Tile" gid="7" x="64" y="320">
636 <properties>
637 <property name="onEnter" value="speed(0.5)"/>
638 <property name="onLeave" value="undo()"/>
639 <property name="door" value="grassfield.area,1,1,0"/>
640 <property name="flags" value="npc_nowalk"/>
641 </properties>
642 </object>
645 xmlChar* type = xmlGetProp(node, BAD_CAST("type"));
646 if (xmlStrncmp(type, BAD_CAST("Tile"), 5)) {
647 Log::err(descriptor, "object type must be Tile");
648 return false;
651 xmlChar* xStr = xmlGetProp(node, BAD_CAST("x"));
652 xmlChar* yStr = xmlGetProp(node, BAD_CAST("y"));
653 // XXX we ignore the object gid... is that okay?
655 // wouldn't have to access tilesets if we had tileDim ourselves
656 long x = atol((const char*)xStr) / tilesets[0].tileDim.x;
657 long y = atol((const char*)yStr) / tilesets[0].tileDim.y;
658 y = y - 1; // bug in tiled? y is 1 too high
660 // We know which Tile is being talked about now... yay
661 Tile& t = map[zpos][y][x];
663 xmlNode* child = node->xmlChildrenNode; // <properties>
664 child = child->xmlChildrenNode; // <property>
665 for (; child != NULL; child = child->next) {
666 xmlChar* name = xmlGetProp(child, BAD_CAST("name"));
667 xmlChar* value = xmlGetProp(child, BAD_CAST("value"));
668 if (!xmlStrncmp(name, BAD_CAST("flags"), 6)) {
669 t.flags = splitTileFlags((const char*)value);
671 else if (!xmlStrncmp(name, BAD_CAST("onEnter"), 8)) {
672 // TODO events
674 else if (!xmlStrncmp(name, BAD_CAST("onLeave"), 8)) {
675 // TODO events
677 else if (!xmlStrncmp(name, BAD_CAST("door"), 5)) {
678 t.door.reset(parseDoor((const char*)value));
679 t.flags |= npc_nowalk;
682 return true;
685 unsigned Area::splitTileFlags(const std::string strOfFlags)
687 std::vector<std::string> strs;
688 strs = splitStr(strOfFlags, ",");
690 unsigned flags = 0x0;
691 BOOST_FOREACH(const std::string& str, strs) {
692 if (str == "nowalk")
693 flags |= nowalk;
695 return flags;
698 Area::Door Area::parseDoor(const std::string dest)
702 Format: destination Area, x, y, z
703 E.g.: "babysfirst.area,1,3,0"
706 std::vector<std::string> strs;
707 strs = splitStr(dest, ",");
709 // TODO: verify the validity of the input string... it's coming from
710 // user land
711 Door door;
712 door.area = strs[0];
713 door.coord.x = atol(strs[1].c_str());
714 door.coord.y = atol(strs[2].c_str());
715 door.coord.z = atol(strs[3].c_str());
716 return door;