feat: faction and activity scanner.

This commit is contained in:
UW Admin 2020-11-15 00:59:05 -05:00
parent 8d7b55e7cf
commit 269878c695
12 changed files with 311 additions and 70 deletions

View file

@ -35,13 +35,9 @@ public:
bool isInRange() { return _inRange; }
void advance(float dt) {
_timeout = glm::max(0.0f, _timeout - dt);
}
void advance(float dt) { _timeout = glm::max(0.0f, _timeout - dt); }
bool isTimedOut() const {
return _timeout < 1E-6;
}
bool isTimedOut() const { return _timeout < 1E-6; }
float distance() const { return _distance; }

View file

@ -34,6 +34,7 @@ public:
void update();
bool empty() const;
size_t size() const { return _actions.size(); }
Action *currentAction();

View file

@ -19,6 +19,7 @@
#include <boost/algorithm/string.hpp>
#include "../../common/log.h"
#include "../../resource/resources.h"
using namespace std;
@ -42,6 +43,7 @@ void CreatureBlueprint::load(const GffStruct &utc) {
_appearance = utc.getInt("Appearance_Type");
_portraitId = utc.getInt("PortraitId", -1);
_factionId = utc.getInt("FactionID", -1);
_conversation = utc.getString("Conversation");
int firstNameStrRef = utc.getInt("FirstName", -1);

View file

@ -41,6 +41,7 @@ public:
const std::vector<std::string> &equipment() const;
int appearance() const;
int portraitId() const;
int factionId() const { return _factionId; }
const std::string &conversation() const;
const CreatureAttributes &attributes() const;
const std::string &onSpawn() const;
@ -53,6 +54,7 @@ private:
std::vector<std::string> _equipment;
int _appearance { 0 };
int _portraitId { -1 };
int _factionId { -1 };
std::string _conversation;
CreatureAttributes _attributes;

View file

@ -28,22 +28,34 @@ namespace reone {
namespace game {
// void Combat::load(const shared_ptr<Area> &area)
// {
// _area = area;
// helper functions
// _activeCombatants.clear();
AttackAction* getAttackAction(shared_ptr<Creature>& combatant) {
return dynamic_cast<AttackAction*>(combatant->actionQueue().currentAction());
}
// // for testing purpose:
// for (auto &creature : area->objectsByType()[ObjectType::Creature]) {
// if (creature->tag().compare("kas22_kinrath_05") == 0 || creature->tag().compare("kas22_czguard_01") == 0
// || creature->tag().compare("kas22_czguard_02") == 0 || creature->tag().compare("kas22_dehno_01") == 0) {
bool isActiveTargetInRange(shared_ptr<Creature>& combatant) {
auto* action = getAttackAction(combatant);
return action && action->isInRange();
}
// _activeCombatants.push_back(move(static_pointer_cast<Creature>(creature)));
// debug(boost::format("creature '%s' with id '%d' added to combatant list!!") % creature->tag() % creature->id());
// }
// }
// }
void duel(shared_ptr<Creature>& attacker, shared_ptr<Creature>& target) {
target->face(*attacker);
attacker->face(*target);
attacker->playAnimation("g8a1");
target->playAnimation("g8g1");
}
void bash(shared_ptr<Creature>& attacker, shared_ptr<Creature>& target) {
attacker->face(*target);
attacker->playAnimation("g8a2");
}
void flinch(shared_ptr<Creature>& target) {
target->playAnimation("g1y1");
}
// END helper functions
Combat::Combat(Area *area, Party *party) : _area(area), _party(party) {
if (!area) {
@ -56,20 +68,66 @@ void Combat::update() {
std::shared_ptr<Creature> pc = _party->player();
if (pc) {
for (auto &cbt : _activeCombatants) AIMaster(cbt); // TODO: use blind cycle
activityScanner();
combatStateMachine(pc);
for (auto &cbt : _activeCombatants) {
combatStateMachine(cbt);
if (_activated) {
for (auto& cbt : _activeCombatants) AIMaster(cbt);
for (auto &cbt : _activeCombatants) combatStateMachine(cbt);
animationSync();
}
animationSync();
}
else {
debug("no pc yet ...");
}
}
void Combat::activityScanner()
{
_activated = !_activeCombatants.empty();
auto &actor = _activated ? _activeCombatants.front() : _party->player();
// TODO: need a better mechanism to query creatures in range
bool stillActive = false;
for (auto &creature : _area->objectsByType()[ObjectType::Creature]) {
if (glm::length(creature->position() - actor->position()) > MAX_DETECT_RANGE) {
continue;
}
// TODO: add line-of-sight requirement
const auto &target = static_pointer_cast<Creature>(creature);
if (getIsEnemy(actor, target)) {
stillActive = true;
if (registerCombatant(target)) { // will fail if already registered
debug(boost::format("combat: registered '%s', faction '%d'")
% target->tag() % static_cast<int>(target->getFaction()));
}
}
}
if (!_activated) return;
// remove actor from _activeCombatants if !active
if (!stillActive) {
actor->setCombatState(CombatState::Idle);
_activeCombatants.pop_front();
_activeCombatantIds.erase(actor->id());
debug(boost::format("combat: deactivated '%s', combat_mode[%d]") % actor->tag() % _activated);
}
else { // rotate _activeCombatants
_activeCombatants.pop_front();
_activeCombatants.push_back(actor);
}
}
void Combat::AIMaster(shared_ptr<Creature> &combatant) {
if (combatant->id() == _party->player()->id()) return;
ActionQueue &cbt_queue = combatant->actionQueue();
// no additional instructions if current queue is empty
@ -78,23 +136,20 @@ void Combat::AIMaster(shared_ptr<Creature> &combatant) {
// otherwise, go to nearest hostile, set meleecommand, unset meleecommand
auto hostile = findNearestHostile(combatant);
if (hostile) cbt_queue.add(make_unique<AttackAction>(hostile, true));
}
AttackAction* getAttackAction(shared_ptr<Creature>& combatant) {
return dynamic_cast<AttackAction*>(combatant->actionQueue().currentAction());
}
bool isActiveTargetInRange(shared_ptr<Creature>& combatant) {
auto* action = getAttackAction(combatant);
return action && action->isInRange();
if (hostile) {
cbt_queue.add(make_unique<AttackAction>(hostile, true));
debug(boost::format("AIMaster: '%s' Queued to attack '%s'") % combatant->tag()
% hostile->tag());
}
}
void Combat::onEnterAttackState(shared_ptr<Creature> &combatant) {
if (!combatant) return;
setStateTimeout(combatant, 1500);
debug(boost::format("'%s' enters Attack state, set_timer") % combatant->tag());
debug(boost::format("'%s' enters Attack state, actionQueueLen[%d], attackAction[%d]")
% combatant->tag() % combatant->actionQueue().size()
% (getAttackAction(combatant) != nullptr)); // TODO: disable redundant info
auto *action = getAttackAction(combatant);
auto &target = action->target();
@ -180,22 +235,6 @@ void Combat::combatStateMachine(shared_ptr<Creature> &combatant) {
}
}
void duel(shared_ptr<Creature>& attacker, shared_ptr<Creature>& target) {
target->face(*attacker);
attacker->face(*target);
attacker->playAnimation("g8a1");
target->playAnimation("g8g1");
}
void bash(shared_ptr<Creature>& attacker, shared_ptr<Creature>& target) {
attacker->face(*target);
attacker->playAnimation("g8a2");
}
void flinch(shared_ptr<Creature>& target) {
target->playAnimation("g1y1");
}
void Combat::animationSync() {
while (!_duelQueue.empty()) {
auto &pr = _duelQueue.front();
@ -214,10 +253,11 @@ shared_ptr<Creature> Combat::findNearestHostile(shared_ptr<Creature> &combatant)
shared_ptr<Creature> closest_target = nullptr;
float min_dist = 1000000;
for (auto &creature : _area->objectsByType()[ObjectType::Creature]) {
for (auto &creature : _activeCombatants) {
if (creature->id() == combatant->id()) continue;
// TODO: if (nonhostile) continue;
if (!getIsEnemy(static_pointer_cast<Creature>(creature), combatant))
continue;
float distance = glm::length(creature->position() - combatant->position()); // TODO: fine tune the distance
if (distance < min_dist) {

View file

@ -20,7 +20,7 @@
#include <unordered_set>
#include <map>
#include "faction.h"
#include "effect.h"
#include "types.h"
#include "object/creature.h"
@ -71,20 +71,46 @@ private:
uint32_t _timestamp;
};
constexpr float MAX_DETECT_RANGE = 20; // TODO: adjust detection distance
class Combat {
public:
Combat(Area *area, Party *party);
/*
* AIMaster -> CombatStateMachine -> effectSync -> animationSync
* Always:
* 0. Update Timers
* 1. Activity Scanner
* 2. Sync Effect
* If Combat Mode Activated:
* 3. AIMaster
* 4. Update CombatStateMachine
* 5. Sync Animation
*/
void update();
/*
* Roles:
* 1. Scan surroundings for hostiles
* 2. Queue Commands (e.g. go to, item consumption, equipment swapping etc.)
* 1. Scan the surrounding of one combatant per 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(std::shared_ptr<Creature> &combatant);
@ -95,13 +121,6 @@ public:
*/
void combatStateMachine(std::shared_ptr<Creature> &combatant);
/*
* 1. Evaluate damage/effects
* 2. Animate damage statistics
* 3. Feedback Text
*/
void effectSync();
/*
* Roles:
* 1. Synchronize dueling and isolated attacks
@ -115,8 +134,21 @@ private:
Area *_area;
Party *_party;
std::list<std::shared_ptr<Creature>> _activeCombatants;
/* combat mode */
bool _activated = false;
/* register to _activeCombatants */
bool registerCombatant(const std::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;
}
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;

114
src/game/faction.cpp Normal file
View file

@ -0,0 +1,114 @@
/*
* Copyright (c) 2020 uwadmin12
*
* 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 "faction.h"
#include "../common/log.h"
namespace reone {
namespace game {
const std::list<Faction> HOSTILE = {
Faction::STANDARD_FACTION_HOSTILE_1, Faction::STANDARD_FACTION_HOSTILE_2 };
const std::list<Faction> FRIENDLY = {
Faction::STANDARD_FACTION_FRIENDLY_1, Faction::STANDARD_FACTION_FRIENDLY_2 };
const std::list<Faction> SURRENDER = {
Faction::STANDARD_FACTION_SURRENDER_1, Faction::STANDARD_FACTION_SURRENDER_2 };
const std::list<Faction> GIZKA = {
Faction::STANDARD_FACTION_GIZKA_1, Faction::STANDARD_FACTION_GIZKA_2 };
std::list<std::pair<Faction, Faction>> groupPairs(std::list<Faction> group1, std::list<Faction> group2) {
std::list<std::pair<Faction, Faction>> aggr;
for (auto f1 : group1) {
for (auto f2 : group2) {
aggr.push_back(std::make_pair(f1, f2));
}
}
return std::move(aggr);
}
std::list<std::pair<Faction, Faction>> propagateHostileLinks() {
std::list<std::pair<Faction, Faction>> aggr;
// aggregate factions that are hostile towards each other?
aggr.splice(aggr.end(), groupPairs(HOSTILE, FRIENDLY));
aggr.splice(aggr.end(), groupPairs(HOSTILE, SURRENDER));
aggr.splice(aggr.end(), groupPairs(HOSTILE, GIZKA));
aggr.splice(aggr.end(), groupPairs(HOSTILE, { Faction::STANDARD_FACTION_ENDAR_SPIRE }));
aggr.splice(aggr.end(), groupPairs(HOSTILE, { Faction::STANDARD_FACTION_PREDATOR }));
aggr.splice(aggr.end(), groupPairs(HOSTILE, { Faction::STANDARD_FACTION_PREY }));
aggr.splice(aggr.end(), groupPairs(FRIENDLY, { Faction::STANDARD_FACTION_PREDATOR }));
aggr.splice(aggr.end(), groupPairs(FRIENDLY, { Faction::STANDARD_FACTION_PREY }));
aggr.splice(aggr.end(), groupPairs(FRIENDLY, { Faction::STANDARD_FACTION_RANCOR }));
aggr.splice(aggr.end(), groupPairs(FRIENDLY, { Faction::STANDARD_FACTION_PTAT_TUSKAN }));
aggr.insert(aggr.end(), std::make_pair(Faction::STANDARD_FACTION_PREDATOR, Faction::STANDARD_FACTION_PREY));
return std::move(aggr);
}
std::vector<std::vector<bool>> initialize() {
std::vector<std::vector<bool>> arr(MAX_NUM_FACTION, std::vector<bool>(MAX_NUM_FACTION, false));
// set insane faction hostile to all factions
for (size_t i = 0; i < MAX_NUM_FACTION; ++i) {
size_t j = static_cast<size_t>(Faction::STANDARD_FACTION_INSANE);
arr[i][j] = true;
arr[j][i] = true;
}
// propagate hostile links to map
for (auto& pr : propagateHostileLinks()) {
size_t i = static_cast<size_t>(pr.first);
size_t j = static_cast<size_t>(pr.second);
arr[i][j] = true;
arr[j][i] = true;
}
return std::move(arr);
}
const std::vector<std::vector<bool>> _hostility = initialize();
bool getIsEnemy(const std::shared_ptr<Creature>& oTarget, const std::shared_ptr<Creature>& oSource) {
if (!oTarget || !oSource) {
debug("getIsEnemy, oTarget or OSource is nullptr");
return false;
}
int s = static_cast<int>(oSource->getFaction());
int t = static_cast<int>(oTarget->getFaction());
if (s < 0 || s >= MAX_NUM_FACTION || t < 0 || t >= MAX_NUM_FACTION) {
debug(boost::format("Source %s Faction: %d") % oSource->tag() % s);
debug(boost::format("Target %s Faction: %d") % oTarget->tag() % t);
return false;
}
return _hostility[s][t];
}
} // namespace game
} // namespace reone

15
src/game/faction.h Normal file
View file

@ -0,0 +1,15 @@
#pragma once
#include "object/creature.h"
namespace reone {
namespace game {
constexpr static size_t MAX_NUM_FACTION = 25;
bool getIsEnemy(const std::shared_ptr<Creature>& oTarget, const std::shared_ptr<Creature>& oSource);
} // namespace game
} // namespace reone

View file

@ -205,11 +205,13 @@ void MainMenu::onModuleSelected(const string &name) {
shared_ptr<Creature> player(_game->objectFactory().newCreature());
player->load(playerCfg);
player->setTag("PLAYER");
player->setFaction(Faction::STANDARD_FACTION_FRIENDLY_1);
party.addMember(player);
party.setPlayer(player);
shared_ptr<Creature> companion(_game->objectFactory().newCreature());
companion->load(companionCfg);
companion->setFaction(Faction::STANDARD_FACTION_FRIENDLY_1);
companion->actionQueue().add(make_unique<FollowAction>(player, 1.0f));
party.addMember(companion);

View file

@ -111,6 +111,16 @@ void Creature::load(const shared_ptr<CreatureBlueprint> &blueprint) {
_attributes = blueprint->attributes();
_onSpawn = blueprint->onSpawn();
_onUserDefined = blueprint->onUserDefined();
// TODO: update to match KOTOR 2
if (blueprint->factionId() < 1 || blueprint->factionId() > 17) {
if (blueprint->factionId() != -1)
debug(boost::format("'%s' with id '%d' has strange factionId(): %d") %tag() % id() % blueprint->factionId());
_factionId = Faction::INVALID_STANDARD_FACTION;
} else {
_factionId = static_cast<Faction>(blueprint->factionId());
}
}
void Creature::loadAppearance(const TwoDaTable &table, int row) {

View file

@ -43,6 +43,28 @@ enum class CombatState {
Staggered
};
// TODO: Factions from KOTOR 2
enum class Faction {
INVALID_STANDARD_FACTION = -1,
STANDARD_FACTION_HOSTILE_1 = 1,
STANDARD_FACTION_FRIENDLY_1 = 2,
STANDARD_FACTION_HOSTILE_2 = 3,
STANDARD_FACTION_FRIENDLY_2 = 4,
STANDARD_FACTION_NEUTRAL = 5,
STANDARD_FACTION_INSANE = 6,
STANDARD_FACTION_PTAT_TUSKAN = 7,
STANDARD_FACTION_GLB_XOR = 8,
STANDARD_FACTION_SURRENDER_1 = 9,
STANDARD_FACTION_SURRENDER_2 = 10,
STANDARD_FACTION_PREDATOR = 11,
STANDARD_FACTION_PREY = 12,
STANDARD_FACTION_TRAP = 13,
STANDARD_FACTION_ENDAR_SPIRE = 14,
STANDARD_FACTION_RANCOR = 15,
STANDARD_FACTION_GIZKA_1 = 16,
STANDARD_FACTION_GIZKA_2 = 17
};
class Creature : public SpatialObject {
public:
enum class MovementType {
@ -110,11 +132,15 @@ public:
// Combat
/* combat animation interruption */
bool isInterrupted() { return _cbtState != CombatState::Idle; }
bool isInterrupted() { return !(_cbtState == CombatState::Idle || _cbtState == CombatState::Cooldown); }
CombatState getCombatState() { return _cbtState; }
void setCombatState(CombatState state) { _cbtState = state; }
Faction getFaction() const { return _factionId; }
void setFaction(Faction faction) { _factionId = faction; }
// const std::deque<std::unique_ptr<Effect>> &getActiveEffects() { return _activeEffects; }
void applyEffect(std::unique_ptr<Effect> &&eff) {
@ -154,6 +180,7 @@ private:
CombatState _cbtState = CombatState::Idle;
std::deque<std::unique_ptr<Effect>> _activeEffects;
Faction _factionId = Faction::INVALID_STANDARD_FACTION;
// END combat

View file

@ -77,12 +77,12 @@ bool Player::handleKeyDown(const SDL_KeyboardEvent &event) {
return true;
case SDL_SCANCODE_C:
_moveRight = true;
return true;
case SDL_SCANCODE_F:
_party->leader()->actionQueue().add(std::make_unique<AttackAction>(
_area->combat().findNearestHostile(_party->leader())));
_party->player()->actionQueue().add(std::make_unique<AttackAction>(_area->combat().findNearestHostile(_party->player())));
return true;
default:
@ -121,7 +121,7 @@ void Player::update(float dt) {
shared_ptr<Creature> partyLeader(_party->leader());
if (!partyLeader) return;
if (_party->leader()->isInterrupted()) return;
if (_party->player()->isInterrupted()) return;
float heading = 0.0f;
bool movement = true;