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
| TILE_NOWALK_BCO_ENTITY
),
52 bool Entity::init(const std::string
& descriptor
)
54 this->descriptor
= descriptor
;
55 return processDescriptor();
60 int millis
= GameWindow::instance().time();
61 phase
->updateFrame(millis
);
62 phase
->frame()->draw(doff
.x
+ r
.x
, doff
.y
+ r
.y
, r
.z
);
66 bool Entity::needsRedraw() const
68 int millis
= GameWindow::instance().time();
69 return redraw
|| phase
->needsRedraw(millis
);
73 static double angleFromXY(double x
, double y
)
78 if (x
!= 0 && y
!= 0) {
82 else if (y
< 0 && x
> 0)
84 else if (y
> 0 && x
< 0)
86 else if (y
> 0 && x
> 0)
105 void Entity::update(unsigned long dt
)
108 switch (conf
.moveMode
) {
121 void Entity::updateTurn(unsigned long)
123 // Entities don't do anything in TILE mode.
126 void Entity::updateTile(unsigned long dt
)
132 double traveled
= speed
* (double)dt
;
133 double destDist
= Gosu::distance(r
.x
, r
.y
, destCoord
.x
, destCoord
.y
);
134 if (destDist
<= traveled
) {
140 double perc
= 1.0 - destDist
/traveled
;
141 unsigned long remt
= (unsigned long)(perc
* (double)dt
);
146 double angle
= angleFromXY(r
.x
- destCoord
.x
,
148 double dx
= cos(angle
);
149 double dy
= -sin(angle
);
151 // Fix inaccurate trig functions. (Why do we have to do this!??)
152 if (-1e-10 < dx
&& dx
< 1e-10)
154 if (-1e-10 < dy
&& dy
< 1e-10)
157 r
.x
+= dx
* traveled
;
158 r
.y
+= dy
* traveled
;
162 void Entity::updateNoTile(unsigned long)
167 const std::string
Entity::getFacing() const
169 return directionStr(facing
);
172 bool Entity::setPhase(const std::string
& name
)
174 AnimationMap::iterator it
;
175 it
= phases
.find(name
);
176 if (it
== phases
.end()) {
177 Log::err(descriptor
, "phase '" + name
+ "' not found");
180 Animation
* newPhase
= &it
->second
;
181 if (phase
!= newPhase
) {
182 int now
= GameWindow::instance().time();
184 phase
->startOver(now
);
192 std::string
Entity::getPhase() const
197 rcoord
Entity::getPixelCoord() const
202 icoord
Entity::getTileCoords_i() const
204 return area
->virt2phys(r
);
207 vicoord
Entity::getTileCoords_vi() const
209 return area
->virt2virt(r
);
212 void Entity::setTileCoords(int x
, int y
)
214 vicoord
virt(x
, y
, r
.z
);
215 if (!area
->inBounds(virt
))
218 r
= area
->virt2virt(virt
);
222 void Entity::setTileCoords(int x
, int y
, double z
)
224 vicoord
virt(x
, y
, z
);
225 if (!area
->inBounds(virt
))
228 r
= area
->virt2virt(virt
);
232 void Entity::setTileCoords(icoord phys
)
234 if (!area
->inBounds(phys
))
237 r
= area
->phys2virt_r(phys
);
241 void Entity::setTileCoords(vicoord virt
)
243 if (!area
->inBounds(virt
))
246 r
= area
->virt2virt(virt
);
250 bool Entity::isMoving() const
252 return moving
|| stillMoving
;
255 void Entity::moveByTile(int x
, int y
)
257 moveByTile(ivec2(x
, y
));
260 void Entity::moveByTile(ivec2 delta
)
266 std::vector
<icoord
> tiles
= frontTiles();
267 BOOST_FOREACH(const icoord
& tile
, tiles
) {
273 setPhase(directionStr(facing
));
277 Area
* Entity::getArea()
282 void Entity::setArea(Area
* a
)
286 setSpeed(speedMul
); // Calculate new speed based on tile size.
290 double Entity::getSpeed() const
295 void Entity::setSpeed(double multiplier
)
297 speedMul
= multiplier
;
299 double tilesPerSecond
= area
->getTileDimensions().x
/ 1000.0;
300 speed
= baseSpeed
* speedMul
* tilesPerSecond
;
304 void Entity::addOnUpdateListener(boost::python::object callable
)
306 updateHooks
.push_back(callable
);
309 Tile
& Entity::getTile() const
311 return area
->getTile(getTileCoords_i());
314 Tile
& Entity::getTile()
316 return area
->getTile(getTileCoords_i());
319 void Entity::setFrozen(bool b
)
324 bool Entity::getFrozen()
329 FlagManip
Entity::exemptManip()
331 return FlagManip(&nowalkExempt
);
334 std::vector
<icoord
> Entity::frontTiles() const
336 std::vector
<icoord
> tiles
;
337 icoord dest
= getTileCoords_i();
338 dest
+= icoord(facing
.x
, facing
.y
, 0);
340 boost::optional
<double> layermod
= getTile().layermodAt(facing
);
342 dest
= area
->virt2phys(vicoord(dest
.x
, dest
.y
, *layermod
));
343 tiles
.push_back(dest
);
347 void Entity::calcDoff()
349 // X-axis is centered on tile.
350 doff
.x
= (area
->getTileDimensions().x
- imgw
) / 2;
351 // Y-axis is aligned with bottom of tile.
352 doff
.y
= area
->getTileDimensions().y
- imgh
- 1;
355 SampleRef
Entity::getSound(const std::string
& name
) const
357 SampleMap::const_iterator it
;
358 it
= sounds
.find(name
);
359 if (it
!= sounds
.end())
365 ivec2
Entity::setFacing(ivec2 facing
)
367 this->facing
= ivec2(
368 Gosu::clamp(facing
.x
, -1, 1),
369 Gosu::clamp(facing
.y
, -1, 1)
374 const std::string
& Entity::directionStr(ivec2 facing
) const
376 return directions
[facing
.y
+1][facing
.x
+1];
379 bool Entity::canMove(icoord dest
)
383 delta
-= getTileCoords_i();
384 ivec2
dxy(delta
.x
, delta
.y
);
385 if (!(inBounds
= area
->inBounds(dest
)) &&
386 !(delta
.z
== 0 && getTile().exitAt(dxy
)))
387 // The tile is off the map.
389 destCoord
= area
->phys2virt_r(dest
);
391 destTile
= &area
->getTile(dest
);
392 return !nowalked(*destTile
);
400 bool Entity::nowalked(Tile
& t
)
402 unsigned flags
= nowalkFlags
& ~nowalkExempt
;
403 return t
.hasFlag(flags
);
406 void Entity::occupy(Tile
& t
)
408 unsigned& old
= getTile().flags
;
410 // XXX: When first placing an Entity after creating it, it will be
411 // relocated from its initial position of <0,0,0>. This will break any
412 // NOWALK_BCO_ENTITY put there by another Entity. More generally,
413 // whenever we have a possibility of two Entities being on top of each
414 // other, this will break it.
415 old
&= ~TILE_NOWALK_BCO_ENTITY
;
416 t
.flags
|= TILE_NOWALK_BCO_ENTITY
;
419 void Entity::preMove()
422 fromTile
= &getTile();
424 rcoord d
= destCoord
;
426 deltaCoord
= area
->virt2virt(d
);
430 // Set z right away so that we're on-level with the square we're
434 // Start moving animation.
435 switch (conf
.moveMode
) {
440 setPhase("moving " + getFacing());
446 fromTile
->onLeaveScripts(this);
448 // Set NOWALK_BCO_ENTITY on the destination tile to "reserve" it,
449 // making it exclusive to us. Do this before we even start animating
454 SampleRef step
= getSound("step");
458 if (conf
.moveMode
== TURN
) {
459 // Movement is instantaneous.
466 void Entity::postMove()
471 boost::optional
<double> layermod
= getTile().layermods
[EXIT_NORMAL
];
476 // Stop moving animation.
478 setPhase(getFacing());
482 destTile
->onEnterScripts(this);
486 // TODO: move teleportation here
490 * moveArea(getExit());
497 void Entity::updateScripts()
499 BOOST_FOREACH(ScriptInst
& script
, updateHooks
) {
500 pythonSetGlobal("Entity", this);
501 pythonSetGlobal("Tile", &getTile());
506 void Entity::tileExitScript()
508 BOOST_FOREACH(ScriptInst
& script
, tileExitHooks
) {
509 pythonSetGlobal("Entity", this);
510 pythonSetGlobal("Tile", &getTile());
515 void Entity::tileEntryScript()
517 BOOST_FOREACH(ScriptInst
& script
, tileEntryHooks
) {
518 pythonSetGlobal("Entity", this);
519 pythonSetGlobal("Tile", &getTile());
526 * DESCRIPTOR CODE BELOW
529 bool Entity::processDescriptor()
531 Resourcer
* rc
= Resourcer::instance();
532 XMLRef doc
= rc
->getXMLDoc(descriptor
, "entity.dtd");
535 const XMLNode root
= doc
->root(); // <entity>
539 for (XMLNode node
= root
.childrenNode(); node
; node
= node
.next()) {
540 if (node
.is("speed")) {
541 ASSERT(node
.doubleContent(&baseSpeed
));
542 setSpeed(speedMul
); // Calculate speed from tile size.
543 } else if (node
.is("sprite")) {
544 ASSERT(processSprite(node
.childrenNode()));
545 } else if (node
.is("sounds")) {
546 ASSERT(processSounds(node
.childrenNode()));
547 } else if (node
.is("scripts")) {
548 ASSERT(processScripts(node
.childrenNode()));
554 bool Entity::processSprite(XMLNode node
)
556 Resourcer
* rc
= Resourcer::instance();
558 for (; node
; node
= node
.next()) {
559 if (node
.is("sheet")) {
560 std::string imageSheet
= node
.content();
561 ASSERT(node
.intAttr("tile_width", &imgw
) &&
562 node
.intAttr("tile_height", &imgh
));
563 ASSERT(rc
->getTiledImage(tiles
, imageSheet
,
565 } else if (node
.is("phases")) {
566 ASSERT(processPhases(node
.childrenNode(), tiles
));
572 bool Entity::processPhases(XMLNode node
, const TiledImage
& tiles
)
574 for (; node
; node
= node
.next())
575 if (node
.is("phase"))
576 ASSERT(processPhase(node
, tiles
));
580 bool Entity::processPhase(const XMLNode node
, const TiledImage
& tiles
)
582 /* Each phase requires a 'name'. Additionally,
583 * one of either 'pos' or 'speed' is needed.
584 * If speed is used, we have sub-elements. We
585 * can't have both pos and speed.
587 const std::string name
= node
.attr("name");
589 Log::err(descriptor
, "<phase> name attribute is empty");
593 const std::string posStr
= node
.attr("pos");
594 const std::string speedStr
= node
.attr("speed");
596 if (posStr
.size() && speedStr
.size()) {
597 Log::err(descriptor
, "pos and speed attributes in "
598 "phase element are mutually exclusive");
600 } else if (posStr
.empty() && speedStr
.empty()) {
601 Log::err(descriptor
, "must have pos or speed attribute "
608 ASSERT(node
.intAttr("pos", &pos
));
609 if (pos
< 0 || (int)tiles
.size() < pos
) {
611 "<phase></phase> index out of bounds");
614 phases
[name
].addFrame(tiles
[pos
]);
618 ASSERT(node
.intAttr("speed", &speed
));
620 int len
= (int)(1000.0/speed
);
621 phases
[name
].setFrameLen(len
);
622 ASSERT(processMembers(node
.childrenNode(),
623 phases
[name
], tiles
));
629 bool Entity::processMembers(XMLNode node
, Animation
& anim
,
630 const TiledImage
& tiles
)
632 for (; node
; node
= node
.next())
633 if (node
.is("member"))
634 ASSERT(processMember(node
, anim
, tiles
));
638 bool Entity::processMember(const XMLNode node
, Animation
& anim
,
639 const TiledImage
& tiles
)
642 ASSERT(node
.intAttr("pos", &pos
));
643 if (pos
< 0 || (int)tiles
.size() < pos
) {
644 Log::err(descriptor
, "<member></member> index out of bounds");
647 anim
.addFrame(tiles
[pos
]);
651 bool Entity::processSounds(XMLNode node
)
653 for (; node
; node
= node
.next())
654 if (node
.is("sound"))
655 ASSERT(processSound(node
));
659 bool Entity::processSound(const XMLNode node
)
661 const std::string name
= node
.attr("name");
662 const std::string filename
= node
.content();
664 Log::err(descriptor
, "<sound> name attribute is empty");
666 } else if (filename
.empty()) {
667 Log::err(descriptor
, "<sound></sound> is empty");
671 Resourcer
* rc
= Resourcer::instance();
672 SampleRef s
= rc
->getSample(filename
);
678 bool Entity::processScripts(XMLNode node
)
680 for (; node
; node
= node
.next())
681 if (node
.is("script"))
682 ASSERT(processScript(node
));
686 bool Entity::processScript(const XMLNode node
)
688 const std::string trigger
= node
.attr("trigger");
689 const std::string filename
= node
.content();
690 if (trigger
.empty()) {
691 Log::err(descriptor
, "<script> trigger attribute is empty");
693 } else if (filename
.empty()) {
694 Log::err(descriptor
, "<script></script> is empty");
698 ScriptInst
script(filename
);
699 if (!script
.validate(descriptor
))
702 if (!addScript(trigger
, script
)) {
704 "unrecognized script trigger: " + trigger
);
711 bool Entity::addScript(const std::string
& trigger
, ScriptInst
& script
)
713 if (boost::iequals(trigger
, "on_update")) {
714 updateHooks
.push_back(script
);
717 if (boost::equals(trigger
, "on_tile_entry")) {
718 tileEntryHooks
.push_back(script
);
721 if (boost::iequals(trigger
, "on_tile_exit")) {
722 tileExitHooks
.push_back(script
);
731 using namespace boost::python
;
733 class_
<Entity
>("Entity")
734 .def("init", &Entity::init
)
735 .add_property("frozen", &Entity::getFrozen
, &Entity::setFrozen
)
736 .add_property("phase", &Entity::getPhase
, &Entity::setPhase
)
737 .add_property("area",
738 make_function(&Entity::getArea
,
739 return_value_policy
<reference_existing_object
>()),
741 .add_property("tile", make_function(
742 static_cast<Tile
& (Entity::*) ()> (&Entity::getTile
),
743 return_value_policy
<reference_existing_object
>()))
744 .add_property("coords", &Entity::getTileCoords_vi
)
745 .add_property("speed", &Entity::getSpeed
, &Entity::setSpeed
)
746 .add_property("moving", &Entity::isMoving
)
747 .add_property("exempt", &Entity::exemptManip
)
749 static_cast<void (Entity::*) (int,int,double)>
750 (&Entity::setTileCoords
))
751 .def("move", static_cast<void (Entity::*) (int,int)>
752 (&Entity::moveByTile
))
753 .def("teleport", static_cast<void (Entity::*) (int,int)>
754 (&Entity::setTileCoords
))
755 .def("add_on_update_listener", &Entity::addOnUpdateListener
)
757 static_cast<void (Entity::*) (int,int)>
758 (&Entity::moveByTile
));