refactor: Refactor combat
Streamline timers, switch from real-time to turn-based combat.
This commit is contained in:
parent
59ca0e8b18
commit
f66791aaa4
20 changed files with 387 additions and 648 deletions
|
@ -67,8 +67,7 @@ set(COMMON_HEADERS
|
|||
src/common/streamreader.h
|
||||
src/common/streamutil.h
|
||||
src/common/streamwriter.h
|
||||
src/common/timermap.h
|
||||
src/common/timerqueue.h
|
||||
src/common/timer.h
|
||||
src/common/types.h)
|
||||
|
||||
set(COMMON_SOURCES
|
||||
|
@ -79,7 +78,8 @@ set(COMMON_SOURCES
|
|||
src/common/random.cpp
|
||||
src/common/streamreader.cpp
|
||||
src/common/streamutil.cpp
|
||||
src/common/streamwriter.cpp)
|
||||
src/common/streamwriter.cpp
|
||||
src/common/timer.cpp)
|
||||
|
||||
add_library(libcommon STATIC ${COMMON_HEADERS} ${COMMON_SOURCES})
|
||||
set_target_properties(libcommon PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
|
||||
|
|
43
src/common/timer.cpp
Normal file
43
src/common/timer.cpp
Normal file
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright (c) 2020 The reone project contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "timer.h"
|
||||
|
||||
#include "glm/common.hpp"
|
||||
|
||||
namespace reone {
|
||||
|
||||
Timer::Timer(float timeout) : _timeout(timeout) {
|
||||
}
|
||||
|
||||
void Timer::update(float dt) {
|
||||
_timeout = glm::max(0.0f, _timeout - dt);
|
||||
}
|
||||
|
||||
void Timer::reset(float timeout) {
|
||||
_timeout = timeout;
|
||||
}
|
||||
|
||||
void Timer::cancel() {
|
||||
_timeout = 0.0f;
|
||||
}
|
||||
|
||||
bool Timer::hasTimedOut() const {
|
||||
return _timeout == 0.0f;
|
||||
}
|
||||
|
||||
} // namespace reone
|
36
src/common/timer.h
Normal file
36
src/common/timer.h
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright (c) 2020 The reone project contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace reone {
|
||||
|
||||
class Timer {
|
||||
public:
|
||||
Timer(float timeout);
|
||||
|
||||
void update(float dt);
|
||||
void reset(float timeout);
|
||||
void cancel();
|
||||
|
||||
bool hasTimedOut() const;
|
||||
|
||||
private:
|
||||
float _timeout;
|
||||
};
|
||||
|
||||
} // namespace reone
|
|
@ -1,61 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 The reone project contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace reone {
|
||||
|
||||
/**
|
||||
* Map-based timer structure (<T> must be hashable).
|
||||
*/
|
||||
template <typename T>
|
||||
struct TimerMap {
|
||||
/* overrides previous obj timeout */
|
||||
void setTimeout(const T &obj, uint32_t tick) {
|
||||
_timer[obj] = _timestamp + tick;
|
||||
}
|
||||
|
||||
/* call once per frame, as soon as possible */
|
||||
void update(uint32_t currentTicks) {
|
||||
_timestamp = currentTicks;
|
||||
|
||||
for (auto it = _timer.begin(); it != _timer.end(); ) {
|
||||
if (it->second < _timestamp) {
|
||||
completed.insert(it->first);
|
||||
it = _timer.erase(it);
|
||||
}
|
||||
else ++it;
|
||||
}
|
||||
}
|
||||
|
||||
bool isRegistered(const T& obj) { return _timer.count(obj) == 1; }
|
||||
|
||||
void cancel(const T& obj) { _timer.erase(obj); }
|
||||
|
||||
/* users are responsible for managing this */
|
||||
std::unordered_set<T> completed;
|
||||
|
||||
private:
|
||||
std::unordered_map<T, uint32_t> _timer;
|
||||
|
||||
uint32_t _timestamp { 0 };
|
||||
};
|
||||
|
||||
} // namespace reone
|
|
@ -1,66 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 The reone project contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <list>
|
||||
#include <queue>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace reone {
|
||||
|
||||
/**
|
||||
* Queue-based timer structure.
|
||||
*/
|
||||
template <typename T>
|
||||
struct TimerQueue {
|
||||
/* does not override previous obj */
|
||||
void setTimeout(const T &obj, uint32_t tick) {
|
||||
_timer.push(std::make_pair(_timestamp + tick, obj));
|
||||
}
|
||||
|
||||
/* call once per frame, as soon as possible */
|
||||
void update(uint32_t currentTicks) {
|
||||
_timestamp = currentTicks;
|
||||
|
||||
while (!_timer.empty() && _timer.top().first < _timestamp) {
|
||||
completed.push_back(_timer.top().second);
|
||||
_timer.pop();
|
||||
}
|
||||
}
|
||||
|
||||
/* users are responsible for managing this */
|
||||
std::list<T> completed;
|
||||
|
||||
private:
|
||||
struct TimerPairCompare {
|
||||
constexpr bool operator()(std::pair<uint32_t, T> const& a,
|
||||
std::pair<uint32_t, T> const& b) const noexcept {
|
||||
return a.first > b.first;
|
||||
} // min heap!
|
||||
};
|
||||
|
||||
std::priority_queue<std::pair<uint32_t, T>,
|
||||
std::vector<std::pair<uint32_t, T>>,
|
||||
TimerPairCompare> _timer;
|
||||
|
||||
uint32_t _timestamp { 0 };
|
||||
};
|
||||
|
||||
} // namespace reone
|
|
@ -19,8 +19,6 @@
|
|||
|
||||
#include "../object/creature.h"
|
||||
|
||||
#include "glm/common.hpp"
|
||||
|
||||
#include "objectaction.h"
|
||||
|
||||
namespace reone {
|
||||
|
@ -29,35 +27,19 @@ namespace game {
|
|||
|
||||
class AttackAction : public ObjectAction {
|
||||
public:
|
||||
AttackAction(const std::shared_ptr<Creature> &object, float distance = 3.2f, float timeout = 6.0f) :
|
||||
AttackAction(const std::shared_ptr<Creature> &object, float range = 1.6f) :
|
||||
ObjectAction(ActionType::AttackObject, object),
|
||||
_distance(distance),
|
||||
_timeout(timeout),
|
||||
_inRange(false) {
|
||||
_range(range) {
|
||||
}
|
||||
|
||||
std::shared_ptr<Creature> target() {
|
||||
std::shared_ptr<Creature> target() const {
|
||||
return std::static_pointer_cast<Creature>(_object);
|
||||
}
|
||||
|
||||
void advance(float dt) {
|
||||
_timeout = glm::max(0.0f, _timeout - dt);
|
||||
}
|
||||
|
||||
bool isTimedOut() const {
|
||||
return _timeout < 1e-6;
|
||||
}
|
||||
|
||||
float distance() const { return _distance; }
|
||||
|
||||
bool isInRange() const { return _inRange; }
|
||||
|
||||
void setAttack() { _inRange = true; }
|
||||
float range() const { return _range; }
|
||||
|
||||
private:
|
||||
float _distance;
|
||||
float _timeout;
|
||||
bool _inRange;
|
||||
float _range;
|
||||
};
|
||||
|
||||
} // namespace game
|
||||
|
|
|
@ -122,10 +122,6 @@ void ActionExecutor::executeMoveToObject(Creature &actor, MoveToObjectAction &ac
|
|||
}
|
||||
|
||||
void ActionExecutor::executeFollow(Creature &actor, FollowAction &action, float dt) {
|
||||
// TODO: continuously queue following if combat inactive
|
||||
if (_game->module()->area()->combat().isActivated()) {
|
||||
action.complete();
|
||||
}
|
||||
SpatialObject &object = *static_cast<SpatialObject *>(action.object());
|
||||
glm::vec3 dest(object.position());
|
||||
float distance = actor.distanceTo(glm::vec2(dest));
|
||||
|
@ -159,27 +155,14 @@ void ActionExecutor::executeStartConversation(Object &actor, StartConversationAc
|
|||
}
|
||||
|
||||
void ActionExecutor::executeAttack(Creature &actor, AttackAction &action, float dt) {
|
||||
if (!action.isInRange()) {
|
||||
if (action.isTimedOut()) {
|
||||
action.complete();
|
||||
return;
|
||||
}
|
||||
shared_ptr<Creature> target(action.target());
|
||||
glm::vec3 dest(target->position());
|
||||
|
||||
// pursue and face object one reached
|
||||
action.advance(dt);
|
||||
const SpatialObject* object = dynamic_cast<const SpatialObject*>(action.object());
|
||||
glm::vec3 dest(object->position());
|
||||
|
||||
bool reached = navigateCreature(actor, dest, true, action.distance(), dt);
|
||||
if (reached) {
|
||||
action.setAttack();
|
||||
actor.face(*object);
|
||||
}
|
||||
}
|
||||
navigateCreature(actor, dest, true, action.range(), dt);
|
||||
}
|
||||
|
||||
bool ActionExecutor::navigateCreature(Creature &creature, const glm::vec3 &dest, bool run, float distance, float dt) {
|
||||
if (creature.isInterrupted()) return false;
|
||||
if (creature.isMovementRestricted()) return false;
|
||||
|
||||
const glm::vec3 &origin = creature.position();
|
||||
float distToDest = glm::distance2(origin, dest);
|
||||
|
|
|
@ -17,6 +17,11 @@
|
|||
|
||||
#include "combat.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
|
||||
#include "glm/common.hpp"
|
||||
|
||||
#include "../common/log.h"
|
||||
|
||||
#include "object/area.h"
|
||||
|
@ -28,347 +33,221 @@ namespace reone {
|
|||
|
||||
namespace game {
|
||||
|
||||
// Helper functions
|
||||
constexpr float kRoundDuration = 3.0f;
|
||||
|
||||
static AttackAction *getAttackAction(const shared_ptr<Creature> &combatant) {
|
||||
return dynamic_cast<AttackAction *>(combatant->actionQueue().currentAction());
|
||||
}
|
||||
|
||||
static bool isActiveTargetInRange(const shared_ptr<Creature> &combatant) {
|
||||
auto* action = getAttackAction(combatant);
|
||||
return action && action->isInRange();
|
||||
void Combat::Round::advance(float dt) {
|
||||
time = glm::min(time + dt, kRoundDuration);
|
||||
}
|
||||
|
||||
static void duel(const shared_ptr<Creature> &attacker, const shared_ptr<Creature> &target) {
|
||||
target->face(*attacker);
|
||||
attacker->face(*target);
|
||||
attacker->playAnimation(Creature::Animation::UnarmedAttack1);
|
||||
target->playAnimation(Creature::Animation::UnarmedDodge1);
|
||||
}
|
||||
|
||||
static void bash(const shared_ptr<Creature> &attacker, const shared_ptr<Creature> &target) {
|
||||
attacker->face(*target);
|
||||
attacker->playAnimation(Creature::Animation::UnarmedAttack2);
|
||||
}
|
||||
|
||||
static void flinch(const shared_ptr<Creature> &target) {
|
||||
target->playAnimation(Creature::Animation::Flinch);
|
||||
}
|
||||
|
||||
// END Helper functions
|
||||
|
||||
Combat::Combat(Area *area, Party *party) : _area(area), _party(party) {
|
||||
if (!area) {
|
||||
throw invalid_argument("area must not be null");
|
||||
}
|
||||
if (!party) {
|
||||
throw invalid_argument("party must not be null");
|
||||
}
|
||||
}
|
||||
|
||||
void Combat::update() {
|
||||
updateTimers(SDL_GetTicks());
|
||||
void Combat::update(float dt) {
|
||||
_heartbeatTimer.update(dt);
|
||||
|
||||
shared_ptr<Creature> partyLeader(_party->leader());
|
||||
if (partyLeader) {
|
||||
scanHostility(partyLeader);
|
||||
if (_heartbeatTimer.hasTimedOut()) {
|
||||
_heartbeatTimer.reset(kHeartbeatInterval);
|
||||
updateCombatants();
|
||||
updateAI();
|
||||
}
|
||||
activityScanner();
|
||||
updateRounds(dt);
|
||||
}
|
||||
|
||||
if (isActivated()) {
|
||||
// One AIMaster per frame, rotated by activityScanner
|
||||
if (isAITimerDone(_activeCombatants.front())) {
|
||||
AIMaster(_activeCombatants.front());
|
||||
setAITimeout(_activeCombatants.front());
|
||||
void Combat::updateCombatants() {
|
||||
ObjectList &creatures = _area->getObjectsByType(ObjectType::Creature);
|
||||
for (auto &object : creatures) {
|
||||
shared_ptr<Creature> creature(static_pointer_cast<Creature>(object));
|
||||
|
||||
vector<shared_ptr<Creature>> enemies(getEnemies(*creature));
|
||||
bool hasEnemies = !enemies.empty();
|
||||
|
||||
auto maybeCombatant = _combatantById.find(creature->id());
|
||||
if (maybeCombatant != _combatantById.end()) {
|
||||
if (hasEnemies) {
|
||||
maybeCombatant->second->enemies = move(enemies);
|
||||
} else {
|
||||
_combatantById.erase(maybeCombatant);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
for (auto &cbt : _activeCombatants) {
|
||||
combatStateMachine(cbt);
|
||||
if (hasEnemies) {
|
||||
auto combatant = make_shared<Combatant>();
|
||||
combatant->creature = creature;
|
||||
combatant->enemies = move(enemies);
|
||||
_combatantById.insert(make_pair(creature->id(), move(combatant)));
|
||||
}
|
||||
animationSync();
|
||||
|
||||
// rotate _activeCombatants
|
||||
_activeCombatants.push_back(_activeCombatants.front());
|
||||
_activeCombatants.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
void Combat::updateTimers(uint32_t currentTicks) {
|
||||
_stateTimers.update(currentTicks);
|
||||
_effectDelayTimers.update(currentTicks);
|
||||
_deactivationTimers.update(currentTicks);
|
||||
_aiTimers.update(currentTicks);
|
||||
|
||||
for (const auto &id : _stateTimers.completed) {
|
||||
if (--(_pendingStates[id]) == 0)
|
||||
_pendingStates.erase(id);
|
||||
void Combat::updateAI() {
|
||||
for (auto &pair : _combatantById) {
|
||||
updateCombatantAI(*pair.second);
|
||||
}
|
||||
_stateTimers.completed.clear();
|
||||
|
||||
for (auto &pr : _effectDelayTimers.completed) {
|
||||
if (!pr->first) continue;
|
||||
|
||||
pr->first->applyEffect(move(pr->second));
|
||||
pr->second = nullptr; // dangling?
|
||||
_effectDelayIndex.erase(pr);
|
||||
}
|
||||
_effectDelayTimers.completed.clear();
|
||||
|
||||
for (auto &id : _aiTimers.completed) {
|
||||
_pendingAITimers.erase(id);
|
||||
}
|
||||
_aiTimers.completed.clear();
|
||||
}
|
||||
|
||||
bool Combat::scanHostility(const shared_ptr<Creature> &subject) {
|
||||
bool stillActive = false;
|
||||
void Combat::updateCombatantAI(Combatant &combatant) {
|
||||
shared_ptr<Creature> creature(combatant.creature);
|
||||
ActionQueue &actions = creature->actionQueue();
|
||||
|
||||
Action *action = actions.currentAction();
|
||||
if (action && action->type() == ActionType::AttackObject) return;
|
||||
|
||||
shared_ptr<Creature> enemy(getNearestEnemy(combatant));
|
||||
if (!enemy) return;
|
||||
|
||||
actions.clear();
|
||||
actions.add(make_unique<AttackAction>(enemy));
|
||||
|
||||
debug(boost::format("Combat: attack action added: '%s' -> '%s'") % creature->tag() % enemy->tag(), 2);
|
||||
}
|
||||
|
||||
shared_ptr<Creature> Combat::getNearestEnemy(const Combatant &combatant) const {
|
||||
shared_ptr<Creature> result;
|
||||
float minDist = FLT_MAX;
|
||||
|
||||
for (auto &enemy : combatant.enemies) {
|
||||
float dist = enemy->distanceTo(*combatant.creature);
|
||||
if (dist >= minDist) continue;
|
||||
|
||||
result = enemy;
|
||||
minDist = dist;
|
||||
}
|
||||
|
||||
return move(result);
|
||||
}
|
||||
|
||||
vector<shared_ptr<Creature>> Combat::getEnemies(const Creature &combatant, float range) const {
|
||||
vector<shared_ptr<Creature>> result;
|
||||
|
||||
ObjectList creatures(_area->getObjectsByType(ObjectType::Creature));
|
||||
for (auto &object : creatures) {
|
||||
if (object->distanceTo(*subject) > kDetectionRange) continue;
|
||||
if (object->distanceTo(combatant) > range) continue;
|
||||
|
||||
shared_ptr<Creature> creature(static_pointer_cast<Creature>(object));
|
||||
if (!getIsEnemy(*subject, *creature)) continue;
|
||||
if (!getIsEnemy(combatant, *creature)) continue;
|
||||
|
||||
stillActive = true;
|
||||
// TODO: check line-of-sight
|
||||
|
||||
if (registerCombatant(creature)) { // will fail if already registered
|
||||
debug(boost::format("combat: registered '%s', faction '%d'") % creature->tag() % static_cast<int>(creature->faction()));
|
||||
}
|
||||
|
||||
// TODO: add line-of-sight requirement
|
||||
result.push_back(move(creature));
|
||||
}
|
||||
|
||||
return stillActive;
|
||||
return move(result);
|
||||
}
|
||||
|
||||
void Combat::activityScanner() {
|
||||
if (_activeCombatants.empty()) return;
|
||||
void Combat::updateRounds(float dt) {
|
||||
for (auto &pair : _combatantById) {
|
||||
shared_ptr<Combatant> attacker(pair.second);
|
||||
|
||||
shared_ptr<Creature> actor(_activeCombatants.front());
|
||||
bool stillActive = scanHostility(actor);
|
||||
// Check if attacker already participates in a combat round
|
||||
|
||||
// deactivate actor if !active
|
||||
if (!stillActive) { //&& getAttackAction(actor) == nullptr ???
|
||||
if (_deactivationTimers.completed.count(actor->id()) == 1) {
|
||||
_deactivationTimers.completed.erase(actor->id());
|
||||
auto maybeAttackerRound = _roundByAttackerId.find(attacker->creature->id());
|
||||
if (maybeAttackerRound != _roundByAttackerId.end()) continue;
|
||||
|
||||
// deactivation timeout complete
|
||||
actor->setCombatState(CombatState::Idle);
|
||||
// Check if attacker is close enough to attack its target
|
||||
|
||||
_activeCombatants.pop_front();
|
||||
_activeCombatantIds.erase(actor->id());
|
||||
AttackAction *action = getAttackAction(attacker->creature);
|
||||
if (!action) continue;
|
||||
|
||||
debug(boost::format("combat: deactivated '%s', combat_mode[%d]") % actor->tag() % isActivated());
|
||||
}
|
||||
else if (!_deactivationTimers.isRegistered(actor->id())) {
|
||||
_deactivationTimers.setTimeout(actor->id(), kDeactivationTimeout);
|
||||
shared_ptr<Creature> defender(action->target());
|
||||
if (!defender || defender->distanceTo(*attacker->creature) > action->range()) continue;
|
||||
|
||||
debug(boost::format("combat: registered deactivation timer'%s'") % actor->tag());
|
||||
// Check if target is valid combatant
|
||||
|
||||
auto maybeDefender = _combatantById.find(defender->id());
|
||||
if (maybeDefender == _combatantById.end()) continue;
|
||||
|
||||
attacker->target = defender;
|
||||
|
||||
// Create a combat round if not a duel
|
||||
|
||||
auto maybeDefenderRound = _roundByAttackerId.find(defender->id());
|
||||
bool isDuel = maybeDefenderRound != _roundByAttackerId.end() && maybeDefenderRound->second->defender == attacker;
|
||||
|
||||
if (!isDuel) {
|
||||
auto round = make_shared<Round>();
|
||||
round->attacker = attacker;
|
||||
round->defender = maybeDefender->second;
|
||||
_roundByAttackerId.insert(make_pair(attacker->creature->id(), round));
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (_deactivationTimers.isRegistered(actor->id())) {
|
||||
_deactivationTimers.cancel(actor->id());
|
||||
for (auto it = _roundByAttackerId.begin(); it != _roundByAttackerId.end(); ) {
|
||||
shared_ptr<Round> round(it->second);
|
||||
updateRound(*round, dt);
|
||||
|
||||
debug(boost::format("combat: cancelled deactivation timer'%s'") % actor->tag());
|
||||
if (round->state == RoundState::Finished) {
|
||||
round->attacker->target.reset();
|
||||
it = _roundByAttackerId.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Combat::effectSync() {
|
||||
}
|
||||
void Combat::updateRound(Round &round, float dt) {
|
||||
round.advance(dt);
|
||||
|
||||
void Combat::AIMaster(const shared_ptr<Creature> &combatant) {
|
||||
if (combatant->id() == _party->leader()->id()) return;
|
||||
shared_ptr<Creature> attacker(round.attacker->creature);
|
||||
shared_ptr<Creature> defender(round.defender->creature);
|
||||
bool isDuel = round.defender->target == attacker;
|
||||
|
||||
ActionQueue &cbt_queue = combatant->actionQueue();
|
||||
|
||||
//if (cbt_queue.currentAction()) return;
|
||||
|
||||
auto hostile = findNearestHostile(combatant, kDetectionRange);
|
||||
if (hostile) {
|
||||
cbt_queue.add(make_unique<AttackAction>(hostile));
|
||||
debug(boost::format("AIMaster: '%s' Queued to attack '%s'") % combatant->tag()
|
||||
% hostile->tag());
|
||||
}
|
||||
}
|
||||
|
||||
void Combat::setStateTimeout(const shared_ptr<Creature> &creature, uint32_t delayTicks) {
|
||||
if (!creature) return;
|
||||
|
||||
_stateTimers.setTimeout(creature->id(), delayTicks);
|
||||
|
||||
// in case of repetition
|
||||
if (_pendingStates.count(creature->id()) == 0) {
|
||||
_pendingStates[creature->id()] = 0;
|
||||
}
|
||||
++(_pendingStates[creature->id()]);
|
||||
}
|
||||
|
||||
bool Combat::isStateTimerDone(const shared_ptr<Creature> &creature) {
|
||||
if (!creature) return false;
|
||||
|
||||
return _pendingStates.count(creature->id()) == 0;
|
||||
}
|
||||
|
||||
void Combat::setDelayEffectTimeout(
|
||||
unique_ptr<Effect> &&eff,
|
||||
const shared_ptr<Creature> &target,
|
||||
uint32_t delayTicks
|
||||
) {
|
||||
auto index = _effectDelayIndex.insert(
|
||||
_effectDelayIndex.end(),
|
||||
make_pair(target, move(eff)));
|
||||
|
||||
_effectDelayTimers.setTimeout(index, delayTicks);
|
||||
}
|
||||
|
||||
void Combat::setAITimeout(const shared_ptr<Creature> &creature) {
|
||||
if (!creature) return;
|
||||
|
||||
_aiTimers.setTimeout(creature->id(), kAIMasterInterval);
|
||||
_pendingAITimers.insert(creature->id());
|
||||
}
|
||||
|
||||
bool Combat::isAITimerDone(const shared_ptr<Creature> &creature) {
|
||||
if (!creature) return false;
|
||||
|
||||
return _pendingAITimers.count(creature->id()) == 0;
|
||||
}
|
||||
|
||||
void Combat::onEnterAttackState(const shared_ptr<Creature> &combatant) {
|
||||
if (!combatant) return;
|
||||
|
||||
setStateTimeout(combatant, 1500);
|
||||
debug(boost::format("'%s' enters Attack state, actionQueueLen[%d], attackAction[%d]")
|
||||
% combatant->tag() % combatant->actionQueue().size()
|
||||
% (getAttackAction(combatant) != nullptr)); // TODO: disable redundant info
|
||||
|
||||
AttackAction *action = getAttackAction(combatant);
|
||||
shared_ptr<Creature> target(action->target());
|
||||
|
||||
if (target && (target->combatState() == CombatState::Idle || target->combatState() == CombatState::Cooldown)) {
|
||||
_duelQueue.push_back(make_pair(combatant, target));
|
||||
|
||||
// synchronization
|
||||
combatStateMachine(target);
|
||||
} else {
|
||||
_bashQueue.push_back(make_pair(combatant, target));
|
||||
}
|
||||
|
||||
setDelayEffectTimeout(
|
||||
make_unique<DamageEffect>(combatant), target, 500 // dummy delay
|
||||
);
|
||||
|
||||
action->complete();
|
||||
}
|
||||
|
||||
void Combat::onEnterDefenseState(const shared_ptr<Creature> &combatant) {
|
||||
setStateTimeout(combatant, 1500);
|
||||
debug(boost::format("'%s' enters Defense state, set_timer") % combatant->tag());
|
||||
}
|
||||
|
||||
void Combat::onEnterCooldownState(const shared_ptr<Creature> &combatant) {
|
||||
setStateTimeout(combatant, 1500);
|
||||
debug(boost::format("'%s' enters Cooldown state, set_timer") % combatant->tag());
|
||||
}
|
||||
|
||||
void Combat::combatStateMachine(const shared_ptr<Creature> &combatant) {
|
||||
switch (combatant->combatState()) {
|
||||
case CombatState::Idle:
|
||||
for (auto &pr : _duelQueue) { // if combatant is caught in a duel
|
||||
if (pr.second && pr.second->id() == combatant->id()) {
|
||||
combatant->setCombatState(CombatState::Defense);
|
||||
onEnterDefenseState(combatant);
|
||||
return;
|
||||
switch (round.state) {
|
||||
case RoundState::Started:
|
||||
attacker->face(*defender);
|
||||
attacker->setMovementType(Creature::MovementType::None);
|
||||
attacker->setMovementRestricted(true);
|
||||
if (isDuel) {
|
||||
attacker->playAnimation(Creature::Animation::UnarmedAttack1);
|
||||
defender->face(*attacker);
|
||||
defender->setMovementType(Creature::MovementType::None);
|
||||
defender->setMovementRestricted(true);
|
||||
defender->playAnimation(Creature::Animation::UnarmedDodge1);
|
||||
} else {
|
||||
attacker->playAnimation(Creature::Animation::UnarmedAttack2);
|
||||
}
|
||||
}
|
||||
round.state = RoundState::FirstTurn;
|
||||
debug(boost::format("Combat: first round turn started: '%s' -> '%s'") % attacker->tag() % defender->tag(), 2);
|
||||
break;
|
||||
|
||||
if (isActiveTargetInRange(combatant)) {
|
||||
combatant->setCombatState(CombatState::Attack);
|
||||
onEnterAttackState(combatant);
|
||||
}
|
||||
return;
|
||||
|
||||
case CombatState::Attack:
|
||||
if (isStateTimerDone(combatant)) {
|
||||
combatant->setCombatState(CombatState::Cooldown);
|
||||
onEnterCooldownState(combatant);
|
||||
}
|
||||
return;
|
||||
|
||||
case CombatState::Cooldown:
|
||||
for (auto &pr : _duelQueue) { // if combatant is caught in a duel
|
||||
if (pr.second && pr.second->id() == combatant->id()) {
|
||||
combatant->setCombatState(CombatState::Defense);
|
||||
onEnterDefenseState(combatant);
|
||||
return;
|
||||
case RoundState::FirstTurn:
|
||||
if (round.time >= 0.5f * kRoundDuration) {
|
||||
if (isDuel) {
|
||||
defender->face(*attacker);
|
||||
defender->playAnimation(Creature::Animation::UnarmedAttack1);
|
||||
attacker->face(*defender);
|
||||
attacker->playAnimation(Creature::Animation::UnarmedDodge1);
|
||||
}
|
||||
round.state = RoundState::SecondTurn;
|
||||
debug(boost::format("Combat: second round turn started: '%s' -> '%s'") % attacker->tag() % defender->tag(), 2);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
if (isStateTimerDone(combatant)) {
|
||||
combatant->setCombatState(CombatState::Idle);
|
||||
debug(boost::format("'%s' enters Idle state") % combatant->tag());
|
||||
}
|
||||
return;
|
||||
case RoundState::SecondTurn:
|
||||
if (round.time == kRoundDuration) {
|
||||
attacker->setMovementRestricted(false);
|
||||
defender->setMovementRestricted(false);
|
||||
round.state = RoundState::Finished;
|
||||
debug(boost::format("Combat: round finished: '%s' -> '%s'") % attacker->tag() % defender->tag(), 2);
|
||||
}
|
||||
break;
|
||||
|
||||
case CombatState::Defense:
|
||||
if (isStateTimerDone(combatant)) {
|
||||
combatant->setCombatState(CombatState::Idle);
|
||||
debug(boost::format("'%s' enters Idle state") % combatant->tag());
|
||||
|
||||
// synchronization
|
||||
combatStateMachine(combatant);
|
||||
}
|
||||
return;
|
||||
|
||||
default:
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Combat::animationSync() {
|
||||
while (!_duelQueue.empty()) {
|
||||
auto &pr = _duelQueue.front();
|
||||
duel(pr.first, pr.second);
|
||||
_duelQueue.pop_front();
|
||||
}
|
||||
|
||||
while (!_bashQueue.empty()) {
|
||||
auto &pr = _bashQueue.front();
|
||||
duel(pr.first, pr.second);
|
||||
_bashQueue.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<Creature> Combat::findNearestHostile(const shared_ptr<Creature> &combatant, float detectionRange) {
|
||||
shared_ptr<Creature> closest_target = nullptr;
|
||||
float min_dist = detectionRange;
|
||||
|
||||
for (auto &creature : _activeCombatants) {
|
||||
if (creature->id() == combatant->id()) continue;
|
||||
|
||||
if (!getIsEnemy(static_cast<Creature &>(*creature), *combatant))
|
||||
continue;
|
||||
|
||||
float distance = glm::length(creature->position() - combatant->position()); // TODO: fine tune the distance
|
||||
if (distance < min_dist) {
|
||||
min_dist = distance;
|
||||
closest_target = static_pointer_cast<Creature>(creature);
|
||||
}
|
||||
}
|
||||
|
||||
return move(closest_target);
|
||||
}
|
||||
|
||||
bool Combat::isActivated() const {
|
||||
return !_activeCombatants.empty();
|
||||
}
|
||||
|
||||
bool Combat::registerCombatant(const shared_ptr<Creature> &combatant) {
|
||||
auto res = _activeCombatantIds.insert(combatant->id());
|
||||
if (res.second) { // combatant not already in _activeCombatantIds
|
||||
_activeCombatants.push_back(combatant);
|
||||
}
|
||||
return res.second;
|
||||
bool Combat::isActive() const {
|
||||
shared_ptr<Creature> partyLeader(_party->leader());
|
||||
return partyLeader && _combatantById.count(partyLeader->id()) != 0;
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
|
|
|
@ -17,13 +17,13 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <unordered_set>
|
||||
#include <queue>
|
||||
#include <set>
|
||||
|
||||
#include "SDL2/SDL_timer.h"
|
||||
|
||||
#include "../common/timermap.h"
|
||||
#include "../common/timerqueue.h"
|
||||
#include "../common/timer.h"
|
||||
|
||||
#include "enginetype/effect.h"
|
||||
#include "object/creature.h"
|
||||
|
@ -35,8 +35,6 @@ namespace reone {
|
|||
namespace game {
|
||||
|
||||
constexpr float kDetectionRange = 20.0f;
|
||||
constexpr uint32_t kDeactivationTimeout = 10000; // 10s in ticks
|
||||
constexpr uint32_t kAIMasterInterval = 3000; // 3s in ticks
|
||||
|
||||
class Area;
|
||||
class Party;
|
||||
|
@ -45,131 +43,50 @@ class Combat {
|
|||
public:
|
||||
Combat(Area *area, Party *party);
|
||||
|
||||
/**
|
||||
* Always:
|
||||
* 1. Update Timers
|
||||
* 2. Activity Scanner
|
||||
* 3. Sync Effect
|
||||
*
|
||||
* If Combat Mode Activated:
|
||||
* 4. AIMaster
|
||||
* 5. Update CombatStateMachine
|
||||
* 6. Sync Animation
|
||||
*/
|
||||
void update();
|
||||
void update(float dt);
|
||||
|
||||
/**
|
||||
* Roles:
|
||||
* 1. Scan the first of activeCombatants each frame, remove the combatant
|
||||
* if no enemies are in visible range & no action in actionQueue
|
||||
* 2. Add creatures to _activeCombatants, if applicable
|
||||
* 3. Activate/Deactivate global combat mode for party->player
|
||||
*/
|
||||
void activityScanner();
|
||||
|
||||
/**
|
||||
* Roles:
|
||||
* 1. Evaluate damage/effects
|
||||
* 2. Animate damage statistics
|
||||
* 3. Feedback Text
|
||||
*/
|
||||
void effectSync();
|
||||
|
||||
/**
|
||||
* Roles:
|
||||
* 1. Queue Commands (e.g. go to, item consumption, equipment swapping etc.)
|
||||
*/
|
||||
void AIMaster(const std::shared_ptr<Creature> &combatant);
|
||||
|
||||
/**
|
||||
* Roles:
|
||||
* 1. Timed Animation Interrupt Control
|
||||
* 2. Combat signal exchange and synchronization
|
||||
*/
|
||||
void combatStateMachine(const std::shared_ptr<Creature> &combatant);
|
||||
|
||||
/**
|
||||
* Roles:
|
||||
* 1. Synchronize dueling and isolated attacks
|
||||
* 2. Animate knockdown/whirlwind effects, etc.
|
||||
*/
|
||||
void animationSync();
|
||||
|
||||
std::shared_ptr<Creature> findNearestHostile(const std::shared_ptr<Creature> &combatant,
|
||||
float detectionRange = kDetectionRange);
|
||||
|
||||
bool isActivated() const;
|
||||
bool isActive() const;
|
||||
|
||||
private:
|
||||
enum class RoundState {
|
||||
Started,
|
||||
FirstTurn,
|
||||
SecondTurn,
|
||||
Finished
|
||||
};
|
||||
|
||||
struct Round;
|
||||
|
||||
struct Combatant {
|
||||
std::shared_ptr<Creature> creature;
|
||||
std::vector<std::shared_ptr<Creature>> enemies;
|
||||
std::shared_ptr<Creature> target;
|
||||
};
|
||||
|
||||
struct Round {
|
||||
std::shared_ptr<Combatant> attacker;
|
||||
std::shared_ptr<Combatant> defender;
|
||||
RoundState state { RoundState::Started };
|
||||
float time { 0.0f };
|
||||
|
||||
void advance(float dt);
|
||||
};
|
||||
|
||||
Area *_area;
|
||||
Party *_party;
|
||||
|
||||
/* register to _activeCombatants */
|
||||
bool registerCombatant(const std::shared_ptr<Creature> &combatant);
|
||||
Timer _heartbeatTimer { 0.0f };
|
||||
std::map<uint32_t, std::shared_ptr<Combatant>> _combatantById;
|
||||
std::map<uint32_t, std::shared_ptr<Round>> _roundByAttackerId;
|
||||
|
||||
/* register hostiles to activecombatant list, return stillActive */
|
||||
bool scanHostility(const std::shared_ptr<Creature> &subject);
|
||||
|
||||
std::deque<std::shared_ptr<Creature>> _activeCombatants;
|
||||
std::unordered_set<uint32_t> _activeCombatantIds;
|
||||
|
||||
/* queue[ pair( attacker, victim ) ] */
|
||||
std::deque<std::pair<std::shared_ptr<Creature>, std::shared_ptr<Creature>>> _duelQueue;
|
||||
std::deque<std::pair<std::shared_ptr<Creature>, std::shared_ptr<Creature>>> _bashQueue;
|
||||
|
||||
// Timers
|
||||
|
||||
/* to be called once each frame, as soon as possible */
|
||||
void updateTimers(uint32_t currentTicks);
|
||||
|
||||
void setStateTimeout(const std::shared_ptr<Creature> &creature, uint32_t delayTicks);
|
||||
|
||||
bool isStateTimerDone(const std::shared_ptr<Creature> &creature);
|
||||
|
||||
/* structure: { id : #repetition } */
|
||||
std::unordered_map<uint32_t, int> _pendingStates;
|
||||
TimerQueue<uint32_t> _stateTimers; // use creature_id
|
||||
|
||||
void setDelayEffectTimeout(std::unique_ptr<Effect> &&eff, const std::shared_ptr<Creature> &target, uint32_t delayTicks);
|
||||
|
||||
/*
|
||||
* delay the effect application on creature,
|
||||
* structure: [ (creature, effect) ]
|
||||
*/
|
||||
std::list<std::pair<std::shared_ptr<Creature>,
|
||||
std::unique_ptr<Effect>>> _effectDelayIndex;
|
||||
|
||||
TimerQueue<decltype(_effectDelayIndex.begin())> _effectDelayTimers;
|
||||
|
||||
TimerMap<uint32_t> _deactivationTimers;
|
||||
|
||||
void setAITimeout(const std::shared_ptr<Creature> &creature);
|
||||
|
||||
bool isAITimerDone(const std::shared_ptr<Creature>& creature);
|
||||
|
||||
std::unordered_set<uint32_t> _pendingAITimers;
|
||||
TimerQueue<uint32_t> _aiTimers;
|
||||
|
||||
// END Timers
|
||||
|
||||
// State transition
|
||||
|
||||
/**
|
||||
* Assuming target is in range:
|
||||
* 1. Queue duel/bash animation
|
||||
* 2. Queue delayed effects
|
||||
* 3. Set State Timer
|
||||
*/
|
||||
void onEnterAttackState(const std::shared_ptr<Creature> &combatant);
|
||||
|
||||
/* Set State Timer */
|
||||
void onEnterDefenseState(const std::shared_ptr<Creature> &combatant);
|
||||
|
||||
/* Set State Timer */
|
||||
void onEnterCooldownState(const std::shared_ptr<Creature> &combatant);
|
||||
|
||||
// END State transition
|
||||
void updateCombatants();
|
||||
void updateAI();
|
||||
void updateCombatantAI(Combatant &combatant);
|
||||
void updateRounds(float dt);
|
||||
void updateRound(Round &round, float dt);
|
||||
|
||||
std::vector<std::shared_ptr<Creature>> getEnemies(const Creature &combatant, float range = kDetectionRange) const;
|
||||
std::shared_ptr<Creature> getNearestEnemy(const Combatant &combatant) const;
|
||||
};
|
||||
|
||||
} // namespace game
|
||||
|
|
|
@ -64,7 +64,7 @@ public:
|
|||
}
|
||||
|
||||
/* for feedback text, mostly */
|
||||
std::shared_ptr<Creature>& getDamager() { return _damager; }
|
||||
std::shared_ptr<Creature> &getDamager() { return _damager; }
|
||||
|
||||
private:
|
||||
std::shared_ptr<Creature> _damager;
|
||||
|
|
|
@ -427,20 +427,10 @@ void Game::update() {
|
|||
float dt = measureFrameTime();
|
||||
|
||||
if (_video) {
|
||||
_video->update(dt);
|
||||
if (_video->isFinished()) {
|
||||
if (_movieAudio) {
|
||||
_movieAudio->stop();
|
||||
_movieAudio.reset();
|
||||
}
|
||||
_video.reset();
|
||||
}
|
||||
} else if (!_musicResRef.empty()) {
|
||||
if (!_music || _music->isStopped()) {
|
||||
_music = AudioPlayer::instance().play(_musicResRef, AudioType::Music);
|
||||
}
|
||||
updateVideo(dt);
|
||||
} else {
|
||||
updateMusic();
|
||||
}
|
||||
|
||||
if (!_nextModule.empty()) {
|
||||
loadNextModule();
|
||||
}
|
||||
|
@ -459,6 +449,26 @@ void Game::update() {
|
|||
_window.update(dt);
|
||||
}
|
||||
|
||||
void Game::updateVideo(float dt) {
|
||||
_video->update(dt);
|
||||
|
||||
if (_video->isFinished()) {
|
||||
if (_movieAudio) {
|
||||
_movieAudio->stop();
|
||||
_movieAudio.reset();
|
||||
}
|
||||
_video.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void Game::updateMusic() {
|
||||
if (_musicResRef.empty()) return;
|
||||
|
||||
if (!_music || _music->isStopped()) {
|
||||
_music = AudioPlayer::instance().play(_musicResRef, AudioType::Music);
|
||||
}
|
||||
}
|
||||
|
||||
void Game::loadNextModule() {
|
||||
JobExecutor::instance().cancel();
|
||||
JobExecutor::instance().await();
|
||||
|
|
|
@ -247,6 +247,8 @@ private:
|
|||
void updateCamera(float dt);
|
||||
void stopMovement();
|
||||
void changeScreen(GameScreen screen);
|
||||
void updateVideo(float dt);
|
||||
void updateMusic();
|
||||
|
||||
std::string getMainMenuMusic() const;
|
||||
std::string getCharacterGenerationMusic() const;
|
||||
|
|
|
@ -545,7 +545,7 @@ void Area::update(float dt) {
|
|||
updateHeartbeat(dt);
|
||||
|
||||
// TODO: enable when polished enough
|
||||
//_combat.update();
|
||||
_combat.update(dt);
|
||||
}
|
||||
|
||||
bool Area::moveCreatureTowards(Creature &creature, const glm::vec2 &dest, bool run, float dt) {
|
||||
|
@ -806,7 +806,7 @@ void Area::updateHeartbeat(float dt) {
|
|||
if (!_onHeartbeat.empty()) {
|
||||
runScript(_onHeartbeat, _id, -1, -1);
|
||||
}
|
||||
_heartbeatTimeout = kRoundDuration;
|
||||
_heartbeatTimeout = kHeartbeatInterval;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ class SceneGraph;
|
|||
|
||||
namespace game {
|
||||
|
||||
const float kRoundDuration = 6.0f;
|
||||
const float kHeartbeatInterval = 6.0f;
|
||||
|
||||
typedef std::unordered_map<std::string, std::shared_ptr<Room>> RoomMap;
|
||||
typedef std::vector<std::shared_ptr<SpatialObject>> ObjectList;
|
||||
|
@ -89,7 +89,7 @@ public:
|
|||
ObjectSelector &objectSelector();
|
||||
const Pathfinder &pathfinder() const;
|
||||
const RoomMap &rooms() const;
|
||||
Combat& combat();
|
||||
Combat &combat();
|
||||
|
||||
// Objects
|
||||
|
||||
|
@ -127,7 +127,7 @@ private:
|
|||
std::unique_ptr<resource::Visibility> _visibility;
|
||||
CameraStyle _cameraStyle;
|
||||
std::string _music;
|
||||
float _heartbeatTimeout { kRoundDuration };
|
||||
float _heartbeatTimeout { kHeartbeatInterval };
|
||||
|
||||
// Scripts
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
#include "../../common/timer.h"
|
||||
#include "../../net/types.h"
|
||||
#include "../../render/models.h"
|
||||
#include "../../render/textures.h"
|
||||
|
@ -367,7 +368,7 @@ void Creature::playAnimation(Animation anim) {
|
|||
animName = "g8a2";
|
||||
break;
|
||||
case Animation::UnarmedDodge1:
|
||||
animName = "g8d1";
|
||||
animName = "g8g1";
|
||||
break;
|
||||
case Animation::Flinch:
|
||||
animName = "g1y1";
|
||||
|
@ -506,6 +507,10 @@ void Creature::clearPath() {
|
|||
_path.reset();
|
||||
}
|
||||
|
||||
void Creature::applyEffect(unique_ptr<Effect> &&eff) {
|
||||
_effects.push_back(move(eff));
|
||||
}
|
||||
|
||||
Gender Creature::gender() const {
|
||||
return _config.gender;
|
||||
}
|
||||
|
@ -560,26 +565,12 @@ void Creature::setFaction(Faction faction) {
|
|||
_faction = faction;
|
||||
}
|
||||
|
||||
bool Creature::isInterrupted() const {
|
||||
switch (_combatState) {
|
||||
case CombatState::Idle:
|
||||
case CombatState::Cooldown:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
bool Creature::isMovementRestricted() const {
|
||||
return _movementRestricted;
|
||||
}
|
||||
|
||||
CombatState Creature::combatState() const {
|
||||
return _combatState;
|
||||
}
|
||||
|
||||
void Creature::setCombatState(CombatState state) {
|
||||
_combatState = state;
|
||||
}
|
||||
|
||||
void Creature::applyEffect(unique_ptr<Effect> &&eff) {
|
||||
_activeEffects.push_back(move(eff));
|
||||
void Creature::setMovementRestricted(bool restricted) {
|
||||
_movementRestricted = restricted;
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
|
|
|
@ -32,6 +32,8 @@
|
|||
|
||||
namespace reone {
|
||||
|
||||
class Timer;
|
||||
|
||||
namespace game {
|
||||
|
||||
class CreatureBlueprint;
|
||||
|
@ -82,6 +84,10 @@ public:
|
|||
void playAnimation(Animation anim);
|
||||
void updateModelAnimation();
|
||||
|
||||
void applyEffect(std::unique_ptr<Effect> &&eff);
|
||||
|
||||
bool isMovementRestricted() const;
|
||||
|
||||
Gender gender() const;
|
||||
int appearance() const;
|
||||
std::shared_ptr<render::Texture> portrait() const;
|
||||
|
@ -95,6 +101,7 @@ public:
|
|||
void setMovementType(MovementType type);
|
||||
void setTalking(bool talking);
|
||||
void setFaction(Faction faction);
|
||||
void setMovementRestricted(bool restricted);
|
||||
|
||||
// Equipment
|
||||
|
||||
|
@ -117,18 +124,6 @@ public:
|
|||
|
||||
// END Pathfinding
|
||||
|
||||
// Combat
|
||||
|
||||
void applyEffect(std::unique_ptr<Effect> &&eff);
|
||||
|
||||
bool isInterrupted() const;
|
||||
|
||||
CombatState combatState() const;
|
||||
|
||||
void setCombatState(CombatState state);
|
||||
|
||||
// END Combat
|
||||
|
||||
private:
|
||||
enum class ModelType {
|
||||
Creature,
|
||||
|
@ -151,6 +146,9 @@ private:
|
|||
CreatureAttributes _attributes;
|
||||
bool _animDirty { true };
|
||||
bool _animFireForget { false };
|
||||
Faction _faction { Faction::Invalid };
|
||||
std::deque<std::unique_ptr<Effect>> _effects;
|
||||
bool _movementRestricted { false };
|
||||
|
||||
// Scripts
|
||||
|
||||
|
@ -158,21 +156,8 @@ private:
|
|||
|
||||
// END Scripts
|
||||
|
||||
// Combat
|
||||
|
||||
CombatState _combatState { CombatState::Idle };
|
||||
std::deque<std::unique_ptr<Effect>> _activeEffects;
|
||||
Faction _faction { Faction::Invalid };
|
||||
|
||||
// END Combat
|
||||
|
||||
// Loading
|
||||
|
||||
void loadAppearance(const resource::TwoDaTable &table, int row);
|
||||
void loadPortrait(int appearance);
|
||||
|
||||
// END Loading
|
||||
|
||||
void updateModel();
|
||||
|
||||
ModelType parseModelType(const std::string &s) const;
|
||||
|
|
|
@ -110,7 +110,7 @@ bool Player::handleKeyUp(const SDL_KeyboardEvent &event) {
|
|||
|
||||
void Player::update(float dt) {
|
||||
shared_ptr<Creature> partyLeader(_party->leader());
|
||||
if (!partyLeader || partyLeader->isInterrupted()) return;
|
||||
if (!partyLeader || partyLeader->isMovementRestricted()) return;
|
||||
|
||||
float heading = 0.0f;
|
||||
bool movement = true;
|
||||
|
|
|
@ -38,7 +38,7 @@ const list<Faction> g_gizkaFactions { Faction::Gizka1, Faction::Gizka2 };
|
|||
|
||||
list<pair<Faction, Faction>> groupPairs(list<Faction> group1, list<Faction> group2) {
|
||||
list<pair<Faction, Faction>> aggr;
|
||||
for (auto& f1 : group1) {
|
||||
for (auto &f1 : group1) {
|
||||
for (auto &f2 : group2) {
|
||||
aggr.push_back(make_pair(f1, f2));
|
||||
}
|
||||
|
@ -77,7 +77,7 @@ vector<vector<bool>> initialize() {
|
|||
}
|
||||
|
||||
// propagate hostile links to map
|
||||
for (auto& pr : propagateHostileLinks()) {
|
||||
for (auto &pr : propagateHostileLinks()) {
|
||||
size_t i = static_cast<size_t>(pr.first);
|
||||
size_t j = static_cast<size_t>(pr.second);
|
||||
|
||||
|
|
|
@ -60,7 +60,7 @@ private:
|
|||
const std::string &name,
|
||||
script::VariableType retType,
|
||||
const std::vector<script::VariableType> &argTypes,
|
||||
const std::function<script::Variable(const std::vector<script::Variable>&, script::ExecutionContext &ctx)> &fn);
|
||||
const std::function<script::Variable(const std::vector<script::Variable> &, script::ExecutionContext &ctx)> &fn);
|
||||
|
||||
void addKotorRoutines();
|
||||
void addTslRoutines();
|
||||
|
|
38
tests/timer.cpp
Normal file
38
tests/timer.cpp
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright (c) 2020 The reone project contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#define BOOST_TEST_MODULE timer
|
||||
|
||||
#include <boost/test/included/unit_test.hpp>
|
||||
|
||||
#include "../src/common/timer.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
using namespace reone;
|
||||
|
||||
BOOST_AUTO_TEST_CASE(test_timer_times_out) {
|
||||
Timer timer(1.0f);
|
||||
|
||||
timer.update(0.5f);
|
||||
|
||||
BOOST_TEST(!timer.hasTimedOut());
|
||||
|
||||
timer.update(0.6f);
|
||||
|
||||
BOOST_TEST(timer.hasTimedOut());
|
||||
}
|
Loading…
Reference in a new issue