refactor: Refactor combat

Streamline timers, switch from real-time to turn-based combat.
This commit is contained in:
Vsevolod Kremianskii 2020-11-18 23:37:45 +07:00
parent 59ca0e8b18
commit f66791aaa4
20 changed files with 387 additions and 648 deletions

View file

@ -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
View 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
View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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);

View file

@ -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

View file

@ -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

View file

@ -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;

View file

@ -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();

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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

View file

@ -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

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -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
View 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());
}