remove Blocks
[Tsunagari.git] / src / area.cpp
blobcc162e52caadbbdde08f991ebce1b1faa4893944
1 /******************************
2 ** Tsunagari Tile Engine **
3 ** area.cpp **
4 ** Copyright 2011 OmegaSDG **
5 ******************************/
7 #include <math.h>
8 #include <vector>
10 #include <boost/foreach.hpp>
11 #include <boost/shared_ptr.hpp>
12 #include <Gosu/Graphics.hpp>
13 #include <Gosu/Image.hpp>
14 #include <Gosu/Math.hpp>
15 #include <Gosu/Timing.hpp>
17 #include "area.h"
18 #include "common.h"
19 #include "entity.h"
20 #include "log.h"
21 #include "resourcer.h"
22 #include "tile.h"
23 #include "window.h"
24 #include "world.h"
26 #define ASSERT(x) if (!(x)) return false
28 // Rename Tile => Square
29 // Introduce new Tile
31 /* NOTE: In the TMX map format used by Tiled, tileset tiles start counting
32 their Y-positions from 0, while layer tiles start counting from 1. I
33 can't imagine why the author did this, but we have to take it into
34 account.
37 Area::Area(Resourcer* rc,
38 World* world,
39 Viewport* view,
40 Player* player,
41 Music* music,
42 const std::string& descriptor)
43 : rc(rc),
44 world(world),
45 view(view),
46 player(player),
47 music(music),
48 descriptor(descriptor),
49 dim(0, 0, 0),
50 tileDim(0, 0),
51 loopX(false),
52 loopY(false)
56 Area::~Area()
60 bool Area::init()
62 return processDescriptor();
65 void Area::buttonDown(const Gosu::Button btn)
67 if (btn == Gosu::kbRight)
68 player->startMovement(icoord(1, 0, 0));
69 else if (btn == Gosu::kbLeft)
70 player->startMovement(icoord(-1, 0, 0));
71 else if (btn == Gosu::kbUp)
72 player->startMovement(icoord(0, -1, 0));
73 else if (btn == Gosu::kbDown)
74 player->startMovement(icoord(0, 1, 0));
77 void Area::buttonUp(const Gosu::Button btn)
79 if (btn == Gosu::kbRight)
80 player->stopMovement(icoord(1, 0, 0));
81 else if (btn == Gosu::kbLeft)
82 player->stopMovement(icoord(-1, 0, 0));
83 else if (btn == Gosu::kbUp)
84 player->stopMovement(icoord(0, -1, 0));
85 else if (btn == Gosu::kbDown)
86 player->stopMovement(icoord(0, 1, 0));
89 void Area::draw()
91 updateTileAnimations();
92 drawTiles();
93 drawEntities();
96 void Area::updateTileAnimations()
98 const int millis = GameWindow::getWindow().time();
99 BOOST_FOREACH(TileType& type, tileTypes)
100 type.anim.updateFrame(millis);
103 bool Area::inBounds(int x, int y, int z) const
105 return ((loopX || (0 <= x && x < dim.x)) &&
106 (loopY || (0 <= y && y < dim.y)) &&
107 0 <= z && z < dim.z);
110 void Area::drawTiles() const
112 const icube_t tiles = visibleTiles();
113 for (int z = tiles.z1; z < tiles.z2; z++) {
114 double depth = idx2depth[z];
115 for (int y = tiles.y1; y < tiles.y2; y++) {
116 for (int x = tiles.x1; x < tiles.x2; x++) {
117 int tx = x, ty = y, tz = z;
118 if (loopX)
119 tx = wrap(0, tx, dim.x);
120 if (loopY)
121 ty = wrap(0, ty, dim.y);
122 if (inBounds(tx, ty, tz))
123 drawTile(map[tz][ty][tx], x, y, depth);
129 void Area::drawTile(const Tile& tile, int x, int y, double depth) const
131 const TileType* type = tile.type;
132 if (type) {
133 const Gosu::Image* img = type->anim.frame();
134 if (img)
135 img->draw((double)x*img->width(),
136 (double)y*img->height(), depth);
140 void Area::drawEntities()
142 player->draw();
145 bool Area::needsRedraw() const
147 if (player->needsRedraw())
148 return true;
150 // Do any onscreen tile types need to update their animations?
151 BOOST_FOREACH(const TileType& type, tileTypes)
152 if (type.needsRedraw(*this))
153 return true;
154 return false;
157 void Area::update(unsigned long dt)
159 music->update();
160 player->update(dt);
161 view->update(dt);
164 icoord Area::getDimensions() const
166 return dim;
169 ivec2 Area::getTileDimensions() const
171 return tileDim;
174 bool Area::tileExists(icoord c) const
176 return inBounds(c.x, c.y, c.z);
179 int Area::depthIndex(double depth) const
181 return depth2idx.find(depth)->second;
184 const Tile& Area::getTile(icoord c) const
186 if (loopX)
187 c.x = wrap(0, c.x, dim.x);
188 if (loopY)
189 c.y = wrap(0, c.y, dim.y);
190 return map[c.z][c.y][c.x];
193 Tile& Area::getTile(icoord c)
195 if (loopX)
196 c.x = wrap(0, c.x, dim.x);
197 if (loopY)
198 c.y = wrap(0, c.y, dim.y);
199 return map[c.z][c.y][c.x];
202 icube_t Area::visibleTiles() const
204 rvec2 screen = view->getVirtRes();
205 rvec2 off = view->getMapOffset();
207 int x1 = (int)floor(off.x / tileDim.x);
208 int y1 = (int)floor(off.y / tileDim.y);
209 int x2 = (int)ceil((screen.x + off.x) / tileDim.x);
210 int y2 = (int)ceil((screen.y + off.y) / tileDim.y);
212 return icube(x1, y1, 0, x2, y2, dim.z);
215 bool Area::loopsInX() const
217 return loopX;
220 bool Area::loopsInY() const
222 return loopY;
225 bool Area::processDescriptor()
227 XMLRef doc;
228 XMLNode root;
230 ASSERT(doc = rc->getXMLDoc(descriptor, "area.dtd"));
231 ASSERT(root = doc->root()); // <map>
233 ASSERT(root.intAttr("width", &dim.x));
234 ASSERT(root.intAttr("height", &dim.y));
235 dim.z = 0;
237 for (XMLNode child = root.childrenNode(); child; child = child.next()) {
238 if (child.is("properties")) {
239 ASSERT(processMapProperties(child));
241 else if (child.is("tileset")) {
242 ASSERT(processTileSet(child));
244 else if (child.is("layer")) {
245 ASSERT(processLayer(child));
247 else if (child.is("objectgroup")) {
248 ASSERT(processObjectGroup(child));
252 return true;
255 void Area::allocateMapLayer()
257 map.push_back(grid_t(dim.y, row_t(dim.x)));
260 bool Area::processMapProperties(XMLNode node)
264 <properties>
265 <property name="author" value="Michael D. Reiley"/>
266 <property name="name" value="Baby's First Area"/>
267 <property name="intro_music" value="intro.music"/>
268 <property name="main_music" value="wind.music"/>
269 <property name="onLoad" value="babysfirst_init()"/>
270 <property name="scripts" value="areainits.event,test.event"/>
271 <property name="loop" value="xy"/>
272 </properties>
274 bool introSet = false;
275 bool mainSet = false;
277 for (XMLNode child = node.childrenNode(); child; child = child.next()) {
278 std::string name = child.attr("name");
279 std::string value = child.attr("value");
280 if (name == "author")
281 author = value;
282 else if (name == "name")
283 this->name = value;
284 else if (name == "intro_music") {
285 music->setIntro(value);
286 introSet = true;
288 else if (name == "main_music") {
289 music->setMain(value);
290 mainSet = true;
292 else if (name == "onLoad")
293 onLoadEvents = value;
294 else if (name == "scripts")
295 scripts = value; // TODO split(), load
296 else if (name == "loop") {
297 loopX = value.find('x') != std::string::npos;
298 loopY = value.find('y') != std::string::npos;
302 if (!introSet)
303 music->setIntro("");
304 if (!mainSet)
305 music->setMain("");
306 return true;
309 bool Area::processTileSet(XMLNode node)
313 <tileset firstgid="1" name="tiles.sheet" tilewidth="64" tileheight="64">
314 <image source="tiles.sheet" width="256" height="256"/>
315 <tile id="14">
317 </tile>
318 </tileset>
321 TiledImage img;
322 int x, y;
324 ASSERT(node.intAttr("tilewidth", &x));
325 ASSERT(node.intAttr("tileheight", &y));
327 if (tileDim && tileDim != ivec2(x, y)) {
328 Log::err(descriptor,
329 "Tileset width/height contradict earlier layer");
330 return false;
332 tileDim = ivec2(x, y);
334 if (tileTypes.empty()) {
335 // Add TileType #0, a transparent tile type that fills map
336 // squares for sections of the map that don't exist.
337 TileType zero;
338 zero.flags = nowalk;
339 tileTypes.push_back(zero);
340 // XXX: Paul 2011-11-13: This tiletype isn't directly used
341 // anymore. Should we remove it?
344 for (XMLNode child = node.childrenNode(); child; child = child.next()) {
345 if (child.is("image")) {
346 std::string source = child.attr("source");
347 rc->getTiledImage(img, source,
348 (unsigned)x, (unsigned)y, true);
350 else if (child.is("tile")) {
351 // FIXME: Ensure img
352 if (img.empty()) {
353 Log::err(descriptor,
354 "Tile processed before tileset image loaded");
355 return false;
358 int id;
359 ASSERT(child.intAttr("id", &id));
361 if (id < 0 || (int)tileTypes.size() +
362 (int)img.size() <= id) {
363 Log::err(descriptor, "tile type id is invalid");
364 return false;
367 // Type ids are given with offset given in terms of
368 // tile from the image rather than in terms of the gid
369 // number, which has an extra type, #0, pushing back
370 // all other types by one.
371 int gid = id + 1;
373 if ((int)tileTypes.size() > gid) {
374 Log::err(descriptor,
375 "tile types must be sorted by id");
376 return false;
379 // Undeclared types have default properties.
380 while ((int)tileTypes.size() < gid)
381 tileTypes.push_back(TileType(img));
383 // Handle this (explicitly declared) type.
384 ASSERT(processTileType(child, img, id));
388 // Handle remaining anonymous items.
389 while (img.size())
390 tileTypes.push_back(TileType(img));
391 return true;
394 bool Area::processTileType(XMLNode node, TiledImage& img, int id)
398 <tile id="8">
399 <properties>
400 <property name="flags" value="nowalk"/>
401 <property name="onEnter" value="skid();speed(2)"/>
402 <property name="onLeave" value="undo()"/>
403 </properties>
404 </tile>
405 <tile id="14">
406 <properties>
407 <property name="members" value="1,2,3,4"/>
408 <property name="speed" value="2"/>
409 </properties>
410 </tile>
413 // The id has already been handled by processTileSet, so we don't have
414 // to worry about it.
416 // Initialize a default TileType, we'll build on that.
417 TileType type(img);
419 XMLNode child = node.childrenNode(); // <properties>
420 for (child = child.childrenNode(); child; child = child.next()) {
421 // Each <property>...
422 std::string name = child.attr("name");
423 std::string value = child.attr("value");
424 if (name == "flags") {
425 type.flags = splitTileFlags(value);
427 else if (name == "onEnter") {
428 if (!rc->resourceExists(value)) {
429 Log::err("Resourcer", "script " + value +
430 " referenced but not found");
431 continue;
433 TileEvent e;
434 e.trigger = onEnter;
435 e.script = value;
436 type.events.push_back(e);
437 type.flags |= hasOnEnter;
439 else if (name == "onLeave") {
440 if (!rc->resourceExists(value)) {
441 Log::err("Resourcer", "script " + value +
442 " referenced but not found");
443 continue;
445 TileEvent e;
446 e.trigger = onLeave;
447 e.script = value;
448 type.events.push_back(e);
449 type.flags |= hasOnLeave;
451 else if (name == "members") {
452 std::string memtemp;
453 std::vector<std::string> members;
454 std::vector<std::string>::iterator it;
455 memtemp = value;
456 members = splitStr(memtemp, ",");
458 // Make sure the first member is this tile.
459 if (atoi(members[0].c_str()) != id) {
460 Log::err(descriptor, "first member of tile"
461 " id " + itostr(id) + " animation must be itself.");
462 return false;
465 // Add frames to our animation.
466 // We already have one from TileType's constructor.
467 for (it = members.begin()+1; it < members.end(); it++) {
468 if (img.empty()) {
469 Log::err(descriptor, "ran out of tiles"
470 "/frames for animated tile");
471 return false;
473 type.anim.addFrame(img[id-atoi(it->c_str())+1]);
476 else if (name == "speed") {
477 double hertz;
478 ASSERT(child.doubleAttr("value", &hertz));
479 int len = (int)(1000.0/hertz);
480 type.anim.setFrameLen(len);
484 tileTypes.push_back(type);
485 return true;
488 bool Area::processLayer(XMLNode node)
492 <layer name="Tiles0" width="5" height="5">
493 <properties>
495 </properties>
496 <data>
497 <tile gid="9"/>
498 <tile gid="9"/>
499 <tile gid="9"/>
501 <tile gid="3"/>
502 <tile gid="9"/>
503 <tile gid="9"/>
504 </data>
505 </layer>
508 int x, y;
509 double depth;
510 ASSERT(node.intAttr("width", &x));
511 ASSERT(node.intAttr("height", &y));
513 if (dim.x != x || dim.y != y) {
514 Log::err(descriptor, "layer x,y size != map x,y size");
515 return false;
518 dim.z++;
519 allocateMapLayer();
521 for (XMLNode child = node.childrenNode(); child; child = child.next()) {
522 if (child.is("properties")) {
523 ASSERT(processLayerProperties(child, &depth));
525 else if (child.is("data")) {
526 ASSERT(processLayerData(child, dim.z - 1));
530 return true;
533 bool Area::processLayerProperties(XMLNode node, double* depth)
537 <properties>
538 <property name="layer" value="0"/>
539 </properties>
542 // FIXME: REQUIRE layer key.
543 for (XMLNode child = node.childrenNode(); child; child = child.next()) {
544 std::string name = child.attr("name");
545 std::string value = child.attr("value");
546 if (name == "layer") {
547 ASSERT(child.doubleAttr("value", depth));
548 if (depth2idx.find(*depth) != depth2idx.end()) {
549 Log::err(descriptor, "depth used multiple times");
550 return false;
553 depth2idx[*depth] = dim.z - 1;
554 idx2depth.push_back(*depth);
555 // Effectively idx2depth[dim.z - 1] = depth;
559 return true;
562 bool Area::processLayerData(XMLNode node, int z)
566 <data>
567 <tile gid="9"/>
568 <tile gid="9"/>
569 <tile gid="9"/>
571 <tile gid="3"/>
572 <tile gid="9"/>
573 <tile gid="9"/>
574 </data>
577 int x = 0, y = 0;
579 for (XMLNode child = node.childrenNode(); child; child = child.next()) {
580 if (child.is("tile")) {
581 int gid;
582 ASSERT(child.intAttr("gid", &gid));
584 if (gid < 0 || (int)tileTypes.size() <= gid) {
585 Log::err(descriptor, "invalid tile gid");
586 return false;
589 // A gid of zero means there is no tile at this
590 // position on this layer.
591 if (gid > 0) {
592 TileType& type = tileTypes[gid];
593 Tile& tile = map[z][y][x];
594 type.allOfType.push_back(&tile);
595 tile.type = &type;
598 if (++x == dim.x) {
599 x = 0;
600 y++;
605 return true;
608 bool Area::processObjectGroup(XMLNode node)
612 <objectgroup name="Prop0" width="5" height="5">
613 <properties>
614 <property name="layer" value="0.0"/>
615 </properties>
616 <object name="tile2" gid="7" x="64" y="320">
617 <properties>
618 <property name="onEnter" value="speed(0.5)"/>
619 <property name="onLeave" value="undo()"/>
620 <property name="door" value="grassfield.area,1,1,0"/>
621 <property name="flags" value="npc_nowalk"/>
622 </properties>
623 </object>
624 </objectgroup>
627 double invalid = (double)NAN; // Not a number. See <math.h>
628 int x, y;
629 ASSERT(node.intAttr("width", &x));
630 ASSERT(node.intAttr("height", &y));
632 double depth = invalid;
634 if (dim.x != x || dim.y != y) {
635 Log::err(descriptor, "objectgroup x,y size != map x,y size");
636 return false;
639 for (XMLNode child = node.childrenNode(); child; child = child.next()) {
640 if (child.is("properties")) {
641 ASSERT(processObjectGroupProperties(child, &depth));
643 else if (child.is("object")) {
644 ASSERT(depth != invalid);
645 int z = depth2idx[depth];
646 ASSERT(processObject(child, z));
650 return true;
653 bool Area::processObjectGroupProperties(XMLNode node, double* depth)
657 <properties>
658 <property name="layer" value="0.0"/>
659 </properties>
662 for (XMLNode child = node.childrenNode(); child; child = child.next()) {
663 std::string name = child.attr("name");
664 std::string value = child.attr("value");
665 if (name == "layer") {
666 ASSERT(child.doubleAttr("value", depth));
667 if (depth2idx.find(*depth) == depth2idx.end()) {
668 // FIXME: Refactor into function.
669 // Allocate a new layer.
670 dim.z++;
671 allocateMapLayer();
673 depth2idx[*depth] = dim.z - 1;
674 idx2depth.push_back(*depth);
675 // Effectively idx2depth[dim.z - 1] = depth;
679 return true;
682 bool Area::processObject(XMLNode node, int z)
686 <object name="tile2" gid="7" x="64" y="320">
687 <properties>
688 <property name="onEnter" value="speed(0.5)"/>
689 <property name="onLeave" value="undo()"/>
690 <property name="door" value="grassfield.area,1,1,0"/>
691 <property name="flags" value="npc_nowalk"/>
692 </properties>
693 </object>
694 <object name="foo" x="0" y="0" width="64" height="64">
696 </object>
699 // Gather object properties now. Assign them to tiles later.
700 std::vector<TileEvent> events;
701 boost::optional<Door> door;
702 unsigned flags = 0x0;
704 XMLNode child = node.childrenNode(); // <properties>
705 for (child = child.childrenNode(); child; child = child.next()) {
706 // Each <property>...
707 std::string name = child.attr("name");
708 std::string value = child.attr("value");
709 if (name == "flags") {
710 flags = splitTileFlags(value);
712 else if (name == "onEnter") {
713 if (!rc->resourceExists(value)) {
714 Log::err("Resourcer", "script " + value +
715 " referenced but not found");
716 continue;
718 TileEvent e;
719 e.trigger = onEnter;
720 e.script = value;
721 events.push_back(e);
722 flags |= hasOnEnter;
724 else if (name == "onLeave") {
725 if (!rc->resourceExists(value)) {
726 Log::err("Resourcer", "script " + value +
727 " referenced but not found");
728 continue;
730 TileEvent e;
731 e.trigger = onLeave;
732 e.script = value;
733 events.push_back(e);
734 flags |= hasOnLeave;
736 else if (name == "door") {
737 door.reset(parseDoor(value));
738 flags |= npc_nowalk;
742 // Apply these properties directly to one or more tiles in a rectangle
743 // of the map. We don't keep an intermediary "object" object lying
744 // around.
745 int x, y, w, h;
746 ASSERT(node.intAttr("x", &x));
747 ASSERT(node.intAttr("y", &y));
748 x /= tileDim.x;
749 y /= tileDim.y;
751 if (node.hasAttr("gid")) {
752 // This is one of Tiled's "Tile Objects". It is one tile wide
753 // and high.
754 y = y - 1; // Bug in tiled. The y is off by one.
755 w = 1;
756 h = 1;
758 // We don't actually use the object gid. It is supposed to indicate
759 // which tile our object is rendered as, but, for Tsunagari, tile
760 // objects are always transparent and reveal the tile below.
762 else {
763 // This is one of Tiled's "Objects". It has a width and height.
764 ASSERT(node.intAttr("width", &w));
765 ASSERT(node.intAttr("height", &h));
766 w /= tileDim.x;
767 h /= tileDim.y;
770 // We know which Tiles are being talked about now... yay
771 for (int Y = y; Y < y + h; Y++) {
772 for (int X = x; X < x + w; X++) {
773 Tile& tile = map[z][Y][X];
775 tile.flags |= flags;
776 if (door)
777 tile.door = door;
778 BOOST_FOREACH(TileEvent& e, events)
779 tile.events.push_back(e);
783 return true;
786 // FIXME: It can fail, should return bool.
787 unsigned Area::splitTileFlags(const std::string strOfFlags)
789 std::vector<std::string> strs;
790 strs = splitStr(strOfFlags, ",");
792 unsigned flags = 0x0;
793 BOOST_FOREACH(const std::string& str, strs) {
794 if (str == "nowalk")
795 flags |= nowalk;
797 return flags;
800 // FIXME: It can fail, should return bool.
801 Door Area::parseDoor(const std::string dest)
805 Format: destination Area, x, y, z
806 E.g.: "babysfirst.area,1,3,0"
809 std::vector<std::string> strs;
810 strs = splitStr(dest, ",");
812 // TODO: verify the validity of the input string... it's coming from
813 // user land
814 Door door;
815 door.area = strs[0];
816 door.tile.x = atoi(strs[1].c_str());
817 door.tile.y = atoi(strs[2].c_str());
818 door.tile.z = atoi(strs[3].c_str());
819 return door;