1 /******************************
2 ** Tsunagari Tile Engine **
4 ** Copyright 2011 OmegaSDG **
5 ******************************/
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>
21 #include "resourcer.h"
26 #define ASSERT(x) if (!(x)) return false
28 // Rename Tile => Square
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
37 Area::Area(Resourcer
* rc
,
42 const std::string
& descriptor
)
48 descriptor(descriptor
),
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));
91 updateTileAnimations();
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
;
119 tx
= wrap(0, tx
, dim
.x
);
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
;
133 const Gosu::Image
* img
= type
->anim
.frame();
135 img
->draw((double)x
*img
->width(),
136 (double)y
*img
->height(), depth
);
140 void Area::drawEntities()
145 bool Area::needsRedraw() const
147 if (player
->needsRedraw())
150 // Do any onscreen tile types need to update their animations?
151 BOOST_FOREACH(const TileType
& type
, tileTypes
)
152 if (type
.needsRedraw(*this))
157 void Area::update(unsigned long dt
)
164 icoord
Area::getDimensions() const
169 ivec2
Area::getTileDimensions() const
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
187 c
.x
= wrap(0, c
.x
, dim
.x
);
189 c
.y
= wrap(0, c
.y
, dim
.y
);
190 return map
[c
.z
][c
.y
][c
.x
];
193 Tile
& Area::getTile(icoord c
)
196 c
.x
= wrap(0, c
.x
, dim
.x
);
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
220 bool Area::loopsInY() const
225 bool Area::processDescriptor()
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
));
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
));
255 void Area::allocateMapLayer()
257 map
.push_back(grid_t(dim
.y
, row_t(dim
.x
)));
260 bool Area::processMapProperties(XMLNode node
)
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"/>
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")
282 else if (name
== "name")
284 else if (name
== "intro_music") {
285 music
->setIntro(value
);
288 else if (name
== "main_music") {
289 music
->setMain(value
);
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
;
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"/>
324 ASSERT(node
.intAttr("tilewidth", &x
));
325 ASSERT(node
.intAttr("tileheight", &y
));
327 if (tileDim
&& tileDim
!= ivec2(x
, y
)) {
329 "Tileset width/height contradict earlier layer");
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.
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")) {
354 "Tile processed before tileset image loaded");
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");
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.
373 if ((int)tileTypes
.size() > gid
) {
375 "tile types must be sorted by id");
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.
390 tileTypes
.push_back(TileType(img
));
394 bool Area::processTileType(XMLNode node
, TiledImage
& img
, int id
)
400 <property name="flags" value="nowalk"/>
401 <property name="onEnter" value="skid();speed(2)"/>
402 <property name="onLeave" value="undo()"/>
407 <property name="members" value="1,2,3,4"/>
408 <property name="speed" value="2"/>
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.
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");
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");
448 type
.events
.push_back(e
);
449 type
.flags
|= hasOnLeave
;
451 else if (name
== "members") {
453 std::vector
<std::string
> members
;
454 std::vector
<std::string
>::iterator it
;
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.");
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
++) {
469 Log::err(descriptor
, "ran out of tiles"
470 "/frames for animated tile");
473 type
.anim
.addFrame(img
[id
-atoi(it
->c_str())+1]);
476 else if (name
== "speed") {
478 ASSERT(child
.doubleAttr("value", &hertz
));
479 int len
= (int)(1000.0/hertz
);
480 type
.anim
.setFrameLen(len
);
484 tileTypes
.push_back(type
);
488 bool Area::processLayer(XMLNode node
)
492 <layer name="Tiles0" width="5" height="5">
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");
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));
533 bool Area::processLayerProperties(XMLNode node
, double* depth
)
538 <property name="layer" value="0"/>
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");
553 depth2idx
[*depth
] = dim
.z
- 1;
554 idx2depth
.push_back(*depth
);
555 // Effectively idx2depth[dim.z - 1] = depth;
562 bool Area::processLayerData(XMLNode node
, int z
)
579 for (XMLNode child
= node
.childrenNode(); child
; child
= child
.next()) {
580 if (child
.is("tile")) {
582 ASSERT(child
.intAttr("gid", &gid
));
584 if (gid
< 0 || (int)tileTypes
.size() <= gid
) {
585 Log::err(descriptor
, "invalid tile gid");
589 // A gid of zero means there is no tile at this
590 // position on this layer.
592 TileType
& type
= tileTypes
[gid
];
593 Tile
& tile
= map
[z
][y
][x
];
594 type
.allOfType
.push_back(&tile
);
608 bool Area::processObjectGroup(XMLNode node
)
612 <objectgroup name="Prop0" width="5" height="5">
614 <property name="layer" value="0.0"/>
616 <object name="tile2" gid="7" x="64" y="320">
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"/>
627 double invalid
= (double)NAN
; // Not a number. See <math.h>
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");
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
));
653 bool Area::processObjectGroupProperties(XMLNode node
, double* depth
)
658 <property name="layer" value="0.0"/>
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.
673 depth2idx
[*depth
] = dim
.z
- 1;
674 idx2depth
.push_back(*depth
);
675 // Effectively idx2depth[dim.z - 1] = depth;
682 bool Area::processObject(XMLNode node
, int z
)
686 <object name="tile2" gid="7" x="64" y="320">
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"/>
694 <object name="foo" x="0" y="0" width="64" height="64">
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");
724 else if (name
== "onLeave") {
725 if (!rc
->resourceExists(value
)) {
726 Log::err("Resourcer", "script " + value
+
727 " referenced but not found");
736 else if (name
== "door") {
737 door
.reset(parseDoor(value
));
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
746 ASSERT(node
.intAttr("x", &x
));
747 ASSERT(node
.intAttr("y", &y
));
751 if (node
.hasAttr("gid")) {
752 // This is one of Tiled's "Tile Objects". It is one tile wide
754 y
= y
- 1; // Bug in tiled. The y is off by one.
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.
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
));
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
];
778 BOOST_FOREACH(TileEvent
& e
, events
)
779 tile
.events
.push_back(e
);
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
) {
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
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());