1 /*********************************
2 ** Tsunagari Tile Engine **
4 ** Copyright 2011-2012 OmegaSDG **
5 *********************************/
9 #include <boost/algorithm/string.hpp> // for iequals
10 #include <boost/foreach.hpp>
11 #include <Gosu/Image.hpp>
12 #include <Gosu/Math.hpp>
13 #include <Gosu/Timing.hpp>
20 #include "resourcer.h"
24 #define ASSERT(x) if (!(x)) return false
26 static std::string directions
[][3] = {
27 {"up-left", "up", "up-right"},
28 {"left", "", "right"},
29 {"down-left", "down", "down-right"},
40 nowalkFlags(TILE_NOWALK
| TILE_NOWALK_NPC
),
51 bool Entity::init(const std::string
& descriptor
)
53 this->descriptor
= descriptor
;
54 return processDescriptor();
59 int millis
= GameWindow::instance().time();
60 phase
->updateFrame(millis
);
61 phase
->frame()->draw(doff
.x
+ r
.x
, doff
.y
+ r
.y
, r
.z
);
65 bool Entity::needsRedraw() const
67 int millis
= GameWindow::instance().time();
68 return redraw
|| phase
->needsRedraw(millis
);
72 static double angleFromXY(double x
, double y
)
77 if (x
!= 0 && y
!= 0) {
81 else if (y
< 0 && x
> 0)
83 else if (y
> 0 && x
< 0)
85 else if (y
> 0 && x
> 0)
104 void Entity::update(unsigned long dt
)
107 switch (conf
.moveMode
) {
120 void Entity::updateTurn(unsigned long)
122 // Entities don't do anything in TILE mode.
125 void Entity::updateTile(unsigned long dt
)
131 double traveled
= speed
* (double)dt
;
132 double destDist
= Gosu::distance(r
.x
, r
.y
, destCoord
.x
, destCoord
.y
);
133 if (destDist
<= traveled
) {
139 double perc
= 1.0 - destDist
/traveled
;
140 unsigned long remt
= (unsigned long)(perc
* (double)dt
);
145 double angle
= angleFromXY(r
.x
- destCoord
.x
,
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)
153 if (-1e-10 < dy
&& dy
< 1e-10)
156 r
.x
+= dx
* traveled
;
157 r
.y
+= dy
* traveled
;
161 void Entity::updateNoTile(unsigned long)
166 const std::string
Entity::getFacing() const
168 return directionStr(facing
);
171 bool Entity::setPhase(const std::string
& name
)
173 AnimationMap::iterator it
;
174 it
= phases
.find(name
);
175 if (it
== phases
.end()) {
176 Log::err(descriptor
, "phase '" + name
+ "' not found");
179 Animation
* newPhase
= &it
->second
;
180 if (phase
!= newPhase
) {
181 int now
= GameWindow::instance().time();
183 phase
->startOver(now
);
191 std::string
Entity::getPhase() const
196 rcoord
Entity::getPixelCoord() const
201 icoord
Entity::getTileCoords_i() const
203 return area
->virt2phys(r
);
206 vicoord
Entity::getTileCoords_vi() const
208 return area
->virt2virt(r
);
211 void Entity::setTileCoords(int x
, int y
)
213 vicoord
virt(x
, y
, r
.z
);
214 if (!area
->inBounds(virt
))
217 r
= area
->virt2virt(virt
);
220 void Entity::setTileCoords(int x
, int y
, double z
)
222 vicoord
virt(x
, y
, z
);
223 if (!area
->inBounds(virt
))
226 r
= area
->virt2virt(virt
);
229 void Entity::setTileCoords(icoord phys
)
231 if (!area
->inBounds(phys
))
234 r
= area
->phys2virt_r(phys
);
237 void Entity::setTileCoords(vicoord virt
)
239 if (!area
->inBounds(virt
))
242 r
= area
->virt2virt(virt
);
245 bool Entity::isMoving() const
247 return moving
|| stillMoving
;
250 void Entity::moveByTile(int x
, int y
)
252 moveByTile(ivec2(x
, y
));
255 void Entity::moveByTile(ivec2 delta
)
261 std::vector
<icoord
> tiles
= frontTiles();
262 BOOST_FOREACH(const icoord
& tile
, tiles
) {
268 setPhase(directionStr(facing
));
272 Area
* Entity::getArea()
277 void Entity::setArea(Area
* a
)
281 setSpeed(speedMul
); // Calculate new speed based on tile size.
284 double Entity::getSpeed() const
289 void Entity::setSpeed(double multiplier
)
291 speedMul
= multiplier
;
293 double tilesPerSecond
= area
->getTileDimensions().x
/ 1000.0;
294 speed
= baseSpeed
* speedMul
* tilesPerSecond
;
298 void Entity::addOnUpdateListener(boost::python::object callable
)
300 updateHooks
.push_back(callable
);
303 Tile
& Entity::getTile() const
305 return area
->getTile(getTileCoords_i());
308 Tile
& Entity::getTile()
310 return area
->getTile(getTileCoords_i());
313 void Entity::setFrozen(bool b
)
318 bool Entity::getFrozen()
323 FlagManip
Entity::exemptManip()
325 return FlagManip(&nowalkExempt
);
328 std::vector
<icoord
> Entity::frontTiles() const
330 std::vector
<icoord
> tiles
;
331 icoord dest
= getTileCoords_i();
332 dest
+= icoord(facing
.x
, facing
.y
, 0);
334 boost::optional
<double> layermod
= getTile().layermodAt(facing
);
336 dest
= area
->virt2phys(vicoord(dest
.x
, dest
.y
, *layermod
));
337 tiles
.push_back(dest
);
341 void Entity::calcDoff()
343 // X-axis is centered on tile.
344 doff
.x
= (area
->getTileDimensions().x
- imgw
) / 2;
345 // Y-axis is aligned with bottom of tile.
346 doff
.y
= area
->getTileDimensions().y
- imgh
- 1;
349 SampleRef
Entity::getSound(const std::string
& name
) const
351 SampleMap::const_iterator it
;
352 it
= sounds
.find(name
);
353 if (it
!= sounds
.end())
359 ivec2
Entity::setFacing(ivec2 facing
)
361 this->facing
= ivec2(
362 Gosu::clamp(facing
.x
, -1, 1),
363 Gosu::clamp(facing
.y
, -1, 1)
368 const std::string
& Entity::directionStr(ivec2 facing
) const
370 return directions
[facing
.y
+1][facing
.x
+1];
373 bool Entity::canMove(icoord dest
)
377 delta
-= getTileCoords_i();
378 ivec2
dxy(delta
.x
, delta
.y
);
379 if (!(inBounds
= area
->inBounds(dest
)) &&
380 !(delta
.z
== 0 && getTile().exitAt(dxy
)))
381 // The tile is off the map.
383 destCoord
= area
->phys2virt_r(dest
);
385 destTile
= &area
->getTile(dest
);
386 return !nowalked(*destTile
);
394 bool Entity::nowalked(Tile
& t
)
396 unsigned flags
= nowalkFlags
& ~nowalkExempt
;
398 if (flags
& TILE_NOWALK
) {
399 if (t
.hasFlag(TILE_NOWALK
))
402 if (flags
& TILE_NOWALK_PLAYER
) {
403 if (t
.hasFlag(TILE_NOWALK_PLAYER
))
406 if (flags
& TILE_NOWALK_NPC
) {
407 if (t
.hasFlag(TILE_NOWALK_NPC
))
413 void Entity::preMove()
416 fromTile
= &getTile();
418 rcoord d
= destCoord
;
420 deltaCoord
= area
->virt2virt(d
);
424 // Set z right away so that we're on-level with the square we're
428 // Start moving animation.
429 switch (conf
.moveMode
) {
434 setPhase("moving " + getFacing());
440 fromTile
->onLeaveScripts(this);
442 SampleRef step
= getSound("step");
446 if (conf
.moveMode
== TURN
) {
447 // Movement is instantaneous.
454 void Entity::postMove()
459 boost::optional
<double> layermod
= getTile().layermods
[EXIT_NORMAL
];
464 // Stop moving animation.
466 setPhase(getFacing());
470 destTile
->onEnterScripts(this);
474 // TODO: move teleportation here
478 * moveArea(getExit());
485 void Entity::updateScripts()
487 BOOST_FOREACH(ScriptInst
& script
, updateHooks
) {
488 pythonSetGlobal("Entity", this);
489 pythonSetGlobal("Tile", &getTile());
494 void Entity::tileExitScript()
496 BOOST_FOREACH(ScriptInst
& script
, tileExitHooks
) {
497 pythonSetGlobal("Entity", this);
498 pythonSetGlobal("Tile", &getTile());
503 void Entity::tileEntryScript()
505 BOOST_FOREACH(ScriptInst
& script
, tileEntryHooks
) {
506 pythonSetGlobal("Entity", this);
507 pythonSetGlobal("Tile", &getTile());
514 * DESCRIPTOR CODE BELOW
517 bool Entity::processDescriptor()
519 Resourcer
* rc
= Resourcer::instance();
520 XMLRef doc
= rc
->getXMLDoc(descriptor
, "entity.dtd");
523 const XMLNode root
= doc
->root(); // <entity>
527 for (XMLNode node
= root
.childrenNode(); node
; node
= node
.next()) {
528 if (node
.is("speed")) {
529 ASSERT(node
.doubleContent(&baseSpeed
));
530 setSpeed(speedMul
); // Calculate speed from tile size.
531 } else if (node
.is("sprite")) {
532 ASSERT(processSprite(node
.childrenNode()));
533 } else if (node
.is("sounds")) {
534 ASSERT(processSounds(node
.childrenNode()));
535 } else if (node
.is("scripts")) {
536 ASSERT(processScripts(node
.childrenNode()));
542 bool Entity::processSprite(XMLNode node
)
544 Resourcer
* rc
= Resourcer::instance();
546 for (; node
; node
= node
.next()) {
547 if (node
.is("sheet")) {
548 std::string imageSheet
= node
.content();
549 ASSERT(node
.intAttr("tile_width", &imgw
) &&
550 node
.intAttr("tile_height", &imgh
));
551 ASSERT(rc
->getTiledImage(tiles
, imageSheet
,
553 } else if (node
.is("phases")) {
554 ASSERT(processPhases(node
.childrenNode(), tiles
));
560 bool Entity::processPhases(XMLNode node
, const TiledImage
& tiles
)
562 for (; node
; node
= node
.next())
563 if (node
.is("phase"))
564 ASSERT(processPhase(node
, tiles
));
568 bool Entity::processPhase(const XMLNode node
, const TiledImage
& tiles
)
570 /* Each phase requires a 'name'. Additionally,
571 * one of either 'pos' or 'speed' is needed.
572 * If speed is used, we have sub-elements. We
573 * can't have both pos and speed.
575 const std::string name
= node
.attr("name");
577 Log::err(descriptor
, "<phase> name attribute is empty");
581 const std::string posStr
= node
.attr("pos");
582 const std::string speedStr
= node
.attr("speed");
584 if (posStr
.size() && speedStr
.size()) {
585 Log::err(descriptor
, "pos and speed attributes in "
586 "phase element are mutually exclusive");
588 } else if (posStr
.empty() && speedStr
.empty()) {
589 Log::err(descriptor
, "must have pos or speed attribute "
596 ASSERT(node
.intAttr("pos", &pos
));
597 if (pos
< 0 || (int)tiles
.size() < pos
) {
599 "<phase></phase> index out of bounds");
602 phases
[name
].addFrame(tiles
[pos
]);
606 ASSERT(node
.intAttr("speed", &speed
));
608 int len
= (int)(1000.0/speed
);
609 phases
[name
].setFrameLen(len
);
610 ASSERT(processMembers(node
.childrenNode(),
611 phases
[name
], tiles
));
617 bool Entity::processMembers(XMLNode node
, Animation
& anim
,
618 const TiledImage
& tiles
)
620 for (; node
; node
= node
.next())
621 if (node
.is("member"))
622 ASSERT(processMember(node
, anim
, tiles
));
626 bool Entity::processMember(const XMLNode node
, Animation
& anim
,
627 const TiledImage
& tiles
)
630 ASSERT(node
.intAttr("pos", &pos
));
631 if (pos
< 0 || (int)tiles
.size() < pos
) {
632 Log::err(descriptor
, "<member></member> index out of bounds");
635 anim
.addFrame(tiles
[pos
]);
639 bool Entity::processSounds(XMLNode node
)
641 for (; node
; node
= node
.next())
642 if (node
.is("sound"))
643 ASSERT(processSound(node
));
647 bool Entity::processSound(const XMLNode node
)
649 const std::string name
= node
.attr("name");
650 const std::string filename
= node
.content();
652 Log::err(descriptor
, "<sound> name attribute is empty");
654 } else if (filename
.empty()) {
655 Log::err(descriptor
, "<sound></sound> is empty");
659 Resourcer
* rc
= Resourcer::instance();
660 SampleRef s
= rc
->getSample(filename
);
666 bool Entity::processScripts(XMLNode node
)
668 for (; node
; node
= node
.next())
669 if (node
.is("script"))
670 ASSERT(processScript(node
));
674 bool Entity::processScript(const XMLNode node
)
676 const std::string trigger
= node
.attr("trigger");
677 const std::string filename
= node
.content();
678 if (trigger
.empty()) {
679 Log::err(descriptor
, "<script> trigger attribute is empty");
681 } else if (filename
.empty()) {
682 Log::err(descriptor
, "<script></script> is empty");
686 Resourcer
* rc
= Resourcer::instance();
687 if (!rc
->resourceExists(filename
)) {
689 "script not found: " + filename
);
693 if (!addScript(trigger
, filename
)) {
695 "unrecognized script trigger: " + trigger
);
702 bool Entity::addScript(const std::string
& trigger
, const std::string
& filename
)
704 if (boost::iequals(trigger
, "on_update")) {
705 updateHooks
.push_back(ScriptInst(filename
));
708 if (boost::equals(trigger
, "on_tile_entry")) {
709 tileEntryHooks
.push_back(ScriptInst(filename
));
712 if (boost::iequals(trigger
, "on_tile_exit")) {
713 tileExitHooks
.push_back(ScriptInst(filename
));
722 using namespace boost::python
;
724 class_
<Entity
>("Entity")
725 .def("init", &Entity::init
)
726 .add_property("frozen", &Entity::getFrozen
, &Entity::setFrozen
)
727 .add_property("phase", &Entity::getPhase
, &Entity::setPhase
)
728 .add_property("area",
729 make_function(&Entity::getArea
,
730 return_value_policy
<reference_existing_object
>()),
732 .add_property("tile", make_function(
733 static_cast<Tile
& (Entity::*) ()> (&Entity::getTile
),
734 return_value_policy
<reference_existing_object
>()))
735 .add_property("coords", &Entity::getTileCoords_vi
)
736 .add_property("speed", &Entity::getSpeed
, &Entity::setSpeed
)
737 .add_property("moving", &Entity::isMoving
)
738 .add_property("exempt", &Entity::exemptManip
)
740 static_cast<void (Entity::*) (int,int,double)>
741 (&Entity::setTileCoords
))
742 .def("move", static_cast<void (Entity::*) (int,int)>
743 (&Entity::moveByTile
))
744 .def("teleport", static_cast<void (Entity::*) (int,int)>
745 (&Entity::setTileCoords
))
746 .def("add_on_update_listener", &Entity::addOnUpdateListener
)
748 static_cast<void (Entity::*) (int,int)>
749 (&Entity::moveByTile
));