cache constructed Gosu::Image, Gosu::Sample, and xmlDoc objects
[Tsunagari.git] / src / area.cpp
blob181b582ddc8193c84c6a4d56372b1b59b91f9617
1 /******************************
2 ** Tsunagari Tile Engine **
3 ** area.cpp **
4 ** Copyright 2011 OmegaSDG **
5 ******************************/
7 #include <boost/foreach.hpp>
8 #include <boost/shared_ptr.hpp>
9 #include <Gosu/Image.hpp>
11 #include "area.h"
12 #include "common.h"
13 #include "entity.h"
14 #include "log.h"
15 #include "resourcer.h"
16 #include "sprite.h"
17 #include "window.h"
18 #include "world.h"
20 /* NOTE: Tileset tiles start counting their positions from 0, while layer tiles
21 start counting from 1. I can't imagine why the author did this, but we
22 have to take it into account.
25 Area::Area(Resourcer* rc,
26 World* world,
27 Entity* player,
28 const std::string& descriptor)
29 : rc(rc), world(world), player(player), descriptor(descriptor)
31 dim.x = dim.y = dim.z = 0;
32 music_inst = NULL;
35 Area::~Area()
37 if (music_inst)
38 music_inst->stop();
39 delete music_inst;
41 // Delete each Tile. If a Tile has an allocated Door struct, delete
42 // that as well.
43 BOOST_FOREACH(grid_t grid, map) {
44 BOOST_FOREACH(row_t row, grid) {
45 BOOST_FOREACH(Tile* tile, row) {
46 delete tile->door;
47 delete tile;
52 // Each Area owns its own Tileset objects. Delete tileset graphics.
53 BOOST_FOREACH(Tileset tileset, tilesets) {
54 delete tileset.source;
55 BOOST_FOREACH(TileType type, tileset.defaults) {
56 BOOST_FOREACH(Gosu::Image* img, type.graphics) {
57 delete img;
59 // TODO: delete TileEvents when we start using them
64 bool Area::init()
66 if (!processDescriptor())
67 return false;
68 if (!music_main.filename.empty()) {
69 music_buf = rc->getSample(music_main.filename);
70 if (music_buf)
71 music_inst = new Gosu::SampleInstance(music_buf->play(1, 1, music_main.loop));
73 return true;
76 void Area::buttonDown(const Gosu::Button btn)
78 bool attemptingMove = false;
79 coord_t posMove;
81 if (btn == Gosu::kbRight) {
82 posMove = coord(1, 0, 0);
83 attemptingMove = true;
85 else if (btn == Gosu::kbLeft) {
86 posMove = coord(-1, 0, 0);
87 attemptingMove = true;
89 else if (btn == Gosu::kbUp) {
90 posMove = coord(0, -1, 0);
91 attemptingMove = true;
93 else if (btn == Gosu::kbDown) {
94 posMove = coord(0, 1, 0);
95 attemptingMove = true;
98 if (attemptingMove) {
99 coord_t newCoord = player->getCoordsByTile();
100 newCoord.x += posMove.x;
101 newCoord.y += posMove.y;
102 newCoord.z += posMove.z;
103 Tile* dest = getTile(newCoord);
104 if ((dest->flags & player_nowalk) != 0 ||
105 (dest->type->flags & player_nowalk) != 0) {
106 // The tile we're trying to move onto is set as nowalk
107 // for the player. Stop here.
108 return;
110 player->moveByTile(posMove);
114 void Area::draw()
116 GameWindow* window = GameWindow::getWindow();
117 Gosu::Graphics* graphics = &window->graphics();
118 Gosu::Transform trans = translateCoords();
119 graphics->pushTransform(trans);
121 for (unsigned layer = 0; layer != map.size(); layer++)
123 grid_t grid = map[layer];
124 for (unsigned y = 0; y != grid.size(); y++)
126 row_t row = grid[y];
127 for (unsigned x = 0; x != row.size(); x++)
129 // TODO support animations
130 Tile* tile = row[x];
131 Gosu::Image* img = tile->type->graphics[0];
132 img->draw(x*img->width(), y*img->height(), 0);
136 player->draw();
138 graphics->popTransform();
141 //! Returns the number closest to x within the range [low, high].
143 \param low Lowest possible return.
144 \param x Value to be bounded.
145 \param high Highest possible return.
146 \return A number close to x.
147 \sa center() and translateCoords()
149 static double bound(double low, double x, double high)
151 if (low > x)
152 x = low;
153 if (x > high)
154 x = high;
155 return x;
158 static double center(double w, double g, double p)
160 return w>g ? (w-g)/2.0 : bound(w-g, w/2.0-p, 0);
163 Gosu::Transform Area::translateCoords()
165 GameWindow* window = GameWindow::getWindow();
166 Gosu::Graphics* graphics = &window->graphics();
168 double tileWidth = (double)tilesets[0].tiledim.x;
169 double tileHeight = (double)tilesets[0].tiledim.y;
170 double windowWidth = (double)graphics->width() / tileWidth;
171 double windowHeight = (double)graphics->height() / tileHeight;
172 double gridWidth = (double)dim.x;
173 double gridHeight = (double)dim.y;
174 double playerX = (double)player->getCoordsByPixel().x / tileWidth + 0.5;
175 double playerY = (double)player->getCoordsByPixel().y / tileHeight + 0.5;
177 coord_t c;
178 c.x = (long)(center(windowWidth, gridWidth, playerX) * tileWidth);
179 c.y = (long)(center(windowHeight, gridHeight, playerY) * tileHeight);
181 Gosu::Transform trans = Gosu::translate((double)c.x, (double)c.y);
182 return trans;
185 bool Area::needsRedraw() const
187 return player->needsRedraw();
190 bool Area::processDescriptor()
192 XMLDocRef doc = rc->getXMLDoc(descriptor);
193 if (!doc)
194 return false;
196 // Iterate and process children of <map>
197 xmlNode* root = xmlDocGetRootElement(doc.get()); // <map> element
199 xmlChar* width = xmlGetProp(root, BAD_CAST("width"));
200 xmlChar* height = xmlGetProp(root, BAD_CAST("height"));
201 dim.x = atol((const char*)width);
202 dim.y = atol((const char*)height);
204 xmlNode* child = root->xmlChildrenNode;
205 for (; child != NULL; child = child->next) {
206 if (!xmlStrncmp(child->name, BAD_CAST("properties"), 11)) {
207 if (!processMapProperties(child))
208 return false;
210 else if (!xmlStrncmp(child->name, BAD_CAST("tileset"), 8)) {
211 if (!processTileset(child))
212 return false;
214 else if (!xmlStrncmp(child->name, BAD_CAST("layer"), 6)) {
215 if (!processLayer(child))
216 return false;
218 else if (!xmlStrncmp(child->name, BAD_CAST("objectgroup"), 12)) {
219 if (!processObjectGroup(child))
220 return false;
224 return true;
227 bool Area::processMapProperties(xmlNode* node)
231 <properties>
232 <property name="areaspec" value="1"/>
233 <property name="author" value="Michael D. Reiley"/>
234 <property name="name" value="Baby's First Area"/>
235 <property name="music_loop" value="true"/>
236 <property name="music_main" value="wind.music"/>
237 <property name="onLoad" value="babysfirst_init()"/>
238 <property name="scripts" value="areainits.event,test.event"/>
239 </properties>
242 xmlNode* child = node->xmlChildrenNode;
243 for (; child != NULL; child = child->next) {
244 xmlChar* name = xmlGetProp(child, BAD_CAST("name"));
245 xmlChar* value = xmlGetProp(child, BAD_CAST("value"));
246 if (!xmlStrncmp(name, BAD_CAST("author"), 7))
247 author = (const char*)value;
248 else if (!xmlStrncmp(name, BAD_CAST("name"), 5))
249 this->name = (const char*)value;
250 else if (!xmlStrncmp(name, BAD_CAST("music_loop"), 11))
251 music_main.loop = parseBool((const char*)value);
252 else if (!xmlStrncmp(name, BAD_CAST("music_main"), 11))
253 music_main.filename = (const char*)value;
254 else if (!xmlStrncmp(name, BAD_CAST("onLoad"), 7))
255 onLoadEvents = (const char*)value;
256 else if (!xmlStrncmp(name, BAD_CAST("scripts"), 8))
257 scripts = (const char*)value; // TODO split(), load
259 return true;
262 bool Area::processTileset(xmlNode* node)
266 <tileset firstgid="1" name="tiles.sheet" tilewidth="64" tileheight="64">
267 <image source="tiles.sheet" width="256" height="256"/>
268 <tile id="14">
270 </tile>
271 </tileset>
273 Tileset ts;
274 ts.source = new Gosu::Bitmap;
275 xmlChar* width = xmlGetProp(node, BAD_CAST("tilewidth"));
276 xmlChar* height = xmlGetProp(node, BAD_CAST("tileheight"));
277 ts.tiledim.x = atol((const char*)width);
278 ts.tiledim.y = atol((const char*)height);
280 xmlNode* child = node->xmlChildrenNode;
281 for (; child != NULL; child = child->next) {
282 if (!xmlStrncmp(child->name, BAD_CAST("tile"), 5)) {
283 xmlChar* idstr = xmlGetProp(child, BAD_CAST("id"));
284 unsigned id = (unsigned)atoi((const char*)idstr);
286 // Undeclared TileTypes have default properties.
287 while (ts.defaults.size() != id) {
288 TileType tt = defaultTileType(ts.source,
289 ts.tiledim, (int)ts.defaults.size());
290 ts.defaults.push_back(tt);
293 // Handle explicit TileType
294 if (!processTileType(child, ts))
295 return false;
297 else if (!xmlStrncmp(child->name, BAD_CAST("image"), 6)) {
298 xmlChar* source = xmlGetProp(child, BAD_CAST("source"));
299 rc->getBitmap(*ts.source, (const char*)source);
303 // Generate default tile types in range (m,n] where m is the last
304 // explicitly declared type and n is the number we require.
305 unsigned srcsz = ts.source->width() * ts.source->height();
306 unsigned long tilesz = (unsigned long)(ts.tiledim.x * ts.tiledim.y);
307 while (ts.defaults.size() != srcsz / tilesz) {
308 TileType tt = defaultTileType(ts.source,
309 ts.tiledim, (int)ts.defaults.size());
310 ts.defaults.push_back(tt);
313 tilesets.push_back(ts);
314 return true;
317 Area::TileType Area::defaultTileType(const Gosu::Bitmap* source,
318 coord_t tiledim, int id)
320 unsigned x = (unsigned)((tiledim.x * id) % source->width());
321 unsigned y = (unsigned)((tiledim.y * id) / source->height() * tiledim.y); // ???
323 TileType tt;
324 Gosu::Image* img = rc->bitmapSection(*source, x, y,
325 (unsigned)tiledim.x, (unsigned)tiledim.y, true);
326 tt.graphics.push_back(img);
327 tt.animated = false;
328 tt.ani_speed = 0.0;
329 tt.flags = 0x0;
330 return tt;
333 bool Area::processTileType(xmlNode* node, Tileset& ts)
337 <tile id="8">
338 <properties>
339 <property name="flags" value="nowalk"/>
340 <property name="onEnter" value="skid();speed(2)"/>
341 <property name="onLeave" value="undo()"/>
342 </properties>
343 </tile>
344 <tile id="14">
345 <properties>
346 <property name="animated" value="1"/>
347 <property name="size" value="2"/>
348 <property name="speed" value="2"/>
349 </properties>
350 </tile>
353 // Initialize a default TileType, we'll build on that.
354 TileType tt = defaultTileType(ts.source,
355 ts.tiledim, (int)ts.defaults.size());
357 xmlChar* idstr = xmlGetProp(node, BAD_CAST("id"));
358 unsigned id = (unsigned)atoi((const char*)idstr);
359 if (id != ts.defaults.size()) {
360 // XXX we need to know the Area we're loading...
361 Log::err("unknown area", std::string("expected TileType id ") +
362 itostr((long)ts.defaults.size()) + ", but got " + itostr(id));
363 return false;
366 xmlNode* child = node->xmlChildrenNode; // <properties>
367 child = child->xmlChildrenNode; // <property>
368 for (; child != NULL; child = child->next) {
369 xmlChar* name = xmlGetProp(child, BAD_CAST("name"));
370 xmlChar* value = xmlGetProp(child, BAD_CAST("value"));
371 if (!xmlStrncmp(name, BAD_CAST("flags"), 6)) {
372 tt.flags = splitTileFlags((const char*)value);
374 else if (!xmlStrncmp(name, BAD_CAST("onEnter"), 8)) {
375 // TODO events
377 else if (!xmlStrncmp(name, BAD_CAST("onLeave"), 8)) {
378 // TODO events
380 else if (!xmlStrncmp(name, BAD_CAST("animated"), 9)) {
381 tt.animated = parseBool((const char*)value);
383 else if (!xmlStrncmp(name, BAD_CAST("size"), 5)) {
384 // TODO animation
386 else if (!xmlStrncmp(name, BAD_CAST("speed"), 6)) {
387 tt.ani_speed = atof((const char*)value);
391 ts.defaults.push_back(tt);
392 return true;
395 bool Area::processLayer(xmlNode* node)
399 <layer name="Tiles0" width="5" height="5">
400 <properties>
402 </properties>
403 <data>
404 <tile gid="9"/>
405 <tile gid="9"/>
406 <tile gid="9"/>
408 <tile gid="3"/>
409 <tile gid="9"/>
410 <tile gid="9"/>
411 </data>
412 </layer>
415 xmlChar* width = xmlGetProp(node, BAD_CAST("width"));
416 xmlChar* height = xmlGetProp(node, BAD_CAST("height"));
417 int x = atoi((const char*)width);
418 int y = atoi((const char*)height);
420 if (dim.x != x || dim.y != y) {
421 // XXX we need to know the Area we're loading...
422 Log::err("unknown area", "layer x,y size != map x,y size");
423 return false;
426 xmlNode* child = node->xmlChildrenNode;
427 for (; child != NULL; child = child->next) {
428 if (!xmlStrncmp(child->name, BAD_CAST("properties"), 11)) {
429 if (!processLayerProperties(child))
430 return false;
432 else if (!xmlStrncmp(child->name, BAD_CAST("data"), 5)) {
433 if (!processLayerData(child))
434 return false;
437 return true;
440 bool Area::processLayerProperties(xmlNode* node)
444 <properties>
445 <property name="layer" value="0"/>
446 </properties>
449 xmlNode* child = node->xmlChildrenNode;
450 for (; child != NULL; child = child->next) {
451 xmlChar* name = xmlGetProp(child, BAD_CAST("name"));
452 xmlChar* value = xmlGetProp(child, BAD_CAST("value"));
453 if (!xmlStrncmp(name, BAD_CAST("layer"), 6)) {
454 int depth = atoi((const char*)value);
455 if (depth != dim.z) {
456 Log::err("unknown area", "invalid layer depth");
457 return false;
462 return true;
465 bool Area::processLayerData(xmlNode* node)
469 <data>
470 <tile gid="9"/>
471 <tile gid="9"/>
472 <tile gid="9"/>
474 <tile gid="3"/>
475 <tile gid="9"/>
476 <tile gid="9"/>
477 </data>
480 row_t row;
481 grid_t grid;
483 row.reserve(dim.x);
484 grid.reserve(dim.y);
486 xmlNode* child = node->xmlChildrenNode;
487 for (int i = 1; child != NULL; i++, child = child->next) {
488 if (!xmlStrncmp(child->name, BAD_CAST("tile"), 5)) {
489 xmlChar* gidStr = xmlGetProp(child, BAD_CAST("gid"));
490 unsigned gid = (unsigned)atoi((const char*)gidStr)-1;
491 Tile* t = new Tile;
492 t->type = &tilesets[0].defaults[gid]; // XXX can only access first tileset
493 t->flags = 0x0;
494 t->door = NULL;
495 row.push_back(t);
496 if (row.size() % dim.x == 0) {
497 grid.push_back(row);
498 row.clear();
499 row.reserve(dim.x);
504 map.push_back(grid);
505 dim.z++;
506 return true;
509 bool Area::processObjectGroup(xmlNode* node)
513 <objectgroup name="Prop0" width="5" height="5">
514 <properties>
515 <property name="layer" value="0"/>
516 </properties>
517 <object name="tile2" type="Tile" gid="7" x="64" y="320">
518 <properties>
519 <property name="onEnter" value="speed(0.5)"/>
520 <property name="onLeave" value="undo()"/>
521 <property name="door" value="grassfield.area,1,1,0"/>
522 <property name="flags" value="npc_nowalk"/>
523 </properties>
524 </object>
525 </objectgroup>
528 xmlChar* width = xmlGetProp(node, BAD_CAST("width"));
529 xmlChar* height = xmlGetProp(node, BAD_CAST("height"));
530 int x = atoi((const char*)width);
531 int y = atoi((const char*)height);
533 int zpos = -1;
535 if (dim.x != x || dim.y != y) {
536 // XXX we need to know the Area we're loading...
537 Log::err("unknown area", "objectgroup x,y size != map x,y size");
538 return false;
541 xmlNode* child = node->xmlChildrenNode;
542 for (; child != NULL; child = child->next) {
543 if (!xmlStrncmp(child->name, BAD_CAST("properties"), 11)) {
544 if (!processObjectGroupProperties(child, &zpos))
545 return false;
547 else if (!xmlStrncmp(child->name, BAD_CAST("object"), 7)) {
548 if (zpos == -1 || !processObject(child, zpos))
549 return false;
553 return true;
556 bool Area::processObjectGroupProperties(xmlNode* node, int* zpos)
560 <properties>
561 <property name="layer" value="0"/>
562 </properties>
565 xmlNode* child = node->xmlChildrenNode;
566 for (; child != NULL; child = child->next) {
567 xmlChar* name = xmlGetProp(child, BAD_CAST("name"));
568 xmlChar* value = xmlGetProp(child, BAD_CAST("value"));
569 if (!xmlStrncmp(name, BAD_CAST("layer"), 6)) {
570 int layer = atoi((const char*)value);
571 if (0 < layer || layer >= (int)dim.z) {
572 // XXX we need to know the Area we're loading...
573 Log::err("unknown area",
574 "objectgroup must correspond with layer"
576 return false;
578 *zpos = layer;
581 return true;
584 bool Area::processObject(xmlNode* node, int zpos)
588 <object name="tile2" type="Tile" gid="7" x="64" y="320">
589 <properties>
590 <property name="onEnter" value="speed(0.5)"/>
591 <property name="onLeave" value="undo()"/>
592 <property name="door" value="grassfield.area,1,1,0"/>
593 <property name="flags" value="npc_nowalk"/>
594 </properties>
595 </object>
598 xmlChar* type = xmlGetProp(node, BAD_CAST("type"));
599 if (xmlStrncmp(type, BAD_CAST("Tile"), 5)) {
600 Log::err("unknown area", "object type must be Tile");
601 return false;
604 xmlChar* xStr = xmlGetProp(node, BAD_CAST("x"));
605 xmlChar* yStr = xmlGetProp(node, BAD_CAST("y"));
606 // XXX we ignore the object gid... is that okay?
608 // wouldn't have to access tilesets if we had tiledim ourselves
609 long x = atol((const char*)xStr) / tilesets[0].tiledim.x;
610 long y = atol((const char*)yStr) / tilesets[0].tiledim.y;
611 y = y - 1; // bug in tiled? y is 1 too high
613 // We know which Tile is being talked about now... yay
614 Tile* t = map[zpos][y][x];
616 xmlNode* child = node->xmlChildrenNode; // <properties>
617 child = child->xmlChildrenNode; // <property>
618 for (; child != NULL; child = child->next) {
619 xmlChar* name = xmlGetProp(child, BAD_CAST("name"));
620 xmlChar* value = xmlGetProp(child, BAD_CAST("value"));
621 if (!xmlStrncmp(name, BAD_CAST("flags"), 6)) {
622 t->flags = splitTileFlags((const char*)value);
624 else if (!xmlStrncmp(name, BAD_CAST("onEnter"), 8)) {
625 // TODO events
627 else if (!xmlStrncmp(name, BAD_CAST("onLeave"), 8)) {
628 // TODO events
630 else if (!xmlStrncmp(name, BAD_CAST("door"), 5)) {
631 t->door = parseDoor((const char*)value);
632 t->flags |= npc_nowalk;
635 return true;
638 unsigned Area::splitTileFlags(const std::string strOfFlags)
640 std::vector<std::string> strs;
641 strs = splitStr(strOfFlags, ",");
643 unsigned flags = 0x0;
644 BOOST_FOREACH(std::string str, strs)
646 // TODO: reimplement comparisons as a hash table
647 if (str == "nowalk") {
648 flags |= nowalk;
651 return flags;
654 Area::Door* Area::parseDoor(const std::string dest)
656 std::vector<std::string> strs;
657 strs = splitStr(dest, ",");
659 // TODO: verify the validity of the input string... it's coming from
660 // user land
661 Door* door = new Door;
662 door->area = strs[0];
663 door->coord.x = atol(strs[1].c_str());
664 door->coord.y = atol(strs[2].c_str());
665 door->coord.z = atol(strs[3].c_str());
666 return door;
669 coord_t Area::getDimensions() const
671 return dim;
674 Area::Tile* Area::getTile(coord_t c)
676 return map[c.z][c.y][c.x];