From 269878c695c36966e172e43a7013000d8239ca6f Mon Sep 17 00:00:00 2001 From: UW Admin Date: Sun, 15 Nov 2020 00:59:05 -0500 Subject: [PATCH] feat: faction and activity scanner. --- src/game/action/attack.h | 8 +- src/game/actionqueue.h | 1 + src/game/blueprint/creature.cpp | 2 + src/game/blueprint/creature.h | 2 + src/game/combat.cpp | 134 +++++++++++++++++++++----------- src/game/combat.h | 58 ++++++++++---- src/game/faction.cpp | 114 +++++++++++++++++++++++++++ src/game/faction.h | 15 ++++ src/game/gui/mainmenu.cpp | 2 + src/game/object/creature.cpp | 10 +++ src/game/object/creature.h | 29 ++++++- src/game/player.cpp | 6 +- 12 files changed, 311 insertions(+), 70 deletions(-) create mode 100644 src/game/faction.cpp create mode 100644 src/game/faction.h diff --git a/src/game/action/attack.h b/src/game/action/attack.h index 9a7406e9..bb130b75 100644 --- a/src/game/action/attack.h +++ b/src/game/action/attack.h @@ -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; } diff --git a/src/game/actionqueue.h b/src/game/actionqueue.h index 5abe7658..04b4f44d 100644 --- a/src/game/actionqueue.h +++ b/src/game/actionqueue.h @@ -34,6 +34,7 @@ public: void update(); bool empty() const; + size_t size() const { return _actions.size(); } Action *currentAction(); diff --git a/src/game/blueprint/creature.cpp b/src/game/blueprint/creature.cpp index 3b1f3d5a..542bf3ca 100644 --- a/src/game/blueprint/creature.cpp +++ b/src/game/blueprint/creature.cpp @@ -19,6 +19,7 @@ #include +#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); diff --git a/src/game/blueprint/creature.h b/src/game/blueprint/creature.h index 86da552e..f14528d6 100644 --- a/src/game/blueprint/creature.h +++ b/src/game/blueprint/creature.h @@ -41,6 +41,7 @@ public: const std::vector &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 _equipment; int _appearance { 0 }; int _portraitId { -1 }; + int _factionId { -1 }; std::string _conversation; CreatureAttributes _attributes; diff --git a/src/game/combat.cpp b/src/game/combat.cpp index d56d0e36..a5902d12 100644 --- a/src/game/combat.cpp +++ b/src/game/combat.cpp @@ -28,22 +28,34 @@ namespace reone { namespace game { -// void Combat::load(const shared_ptr &area) -// { -// _area = area; +// helper functions -// _activeCombatants.clear(); +AttackAction* getAttackAction(shared_ptr& combatant) { + return dynamic_cast(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& combatant) { + auto* action = getAttackAction(combatant); + return action && action->isInRange(); +} -// _activeCombatants.push_back(move(static_pointer_cast(creature))); -// debug(boost::format("creature '%s' with id '%d' added to combatant list!!") % creature->tag() % creature->id()); -// } -// } -// } +void duel(shared_ptr& attacker, shared_ptr& target) { + target->face(*attacker); + attacker->face(*target); + attacker->playAnimation("g8a1"); + target->playAnimation("g8g1"); +} + +void bash(shared_ptr& attacker, shared_ptr& target) { + attacker->face(*target); + attacker->playAnimation("g8a2"); +} + +void flinch(shared_ptr& 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 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); + 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(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 &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 &combatant) { // otherwise, go to nearest hostile, set meleecommand, unset meleecommand auto hostile = findNearestHostile(combatant); - if (hostile) cbt_queue.add(make_unique(hostile, true)); -} - -AttackAction* getAttackAction(shared_ptr& combatant) { - return dynamic_cast(combatant->actionQueue().currentAction()); -} - -bool isActiveTargetInRange(shared_ptr& combatant) { - auto* action = getAttackAction(combatant); - return action && action->isInRange(); + if (hostile) { + cbt_queue.add(make_unique(hostile, true)); + debug(boost::format("AIMaster: '%s' Queued to attack '%s'") % combatant->tag() + % hostile->tag()); + } } void Combat::onEnterAttackState(shared_ptr &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 &combatant) { } } -void duel(shared_ptr& attacker, shared_ptr& target) { - target->face(*attacker); - attacker->face(*target); - attacker->playAnimation("g8a1"); - target->playAnimation("g8g1"); -} - -void bash(shared_ptr& attacker, shared_ptr& target) { - attacker->face(*target); - attacker->playAnimation("g8a2"); -} - -void flinch(shared_ptr& target) { - target->playAnimation("g1y1"); -} - void Combat::animationSync() { while (!_duelQueue.empty()) { auto &pr = _duelQueue.front(); @@ -214,10 +253,11 @@ shared_ptr Combat::findNearestHostile(shared_ptr &combatant) shared_ptr 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), combatant)) + continue; float distance = glm::length(creature->position() - combatant->position()); // TODO: fine tune the distance if (distance < min_dist) { diff --git a/src/game/combat.h b/src/game/combat.h index 204584ca..a41b514b 100644 --- a/src/game/combat.h +++ b/src/game/combat.h @@ -20,7 +20,7 @@ #include #include - +#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 &combatant); @@ -95,13 +121,6 @@ public: */ void combatStateMachine(std::shared_ptr &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> _activeCombatants; - + /* combat mode */ + bool _activated = false; + + /* register to _activeCombatants */ + bool registerCombatant(const std::shared_ptr &combatant) { + auto res = _activeCombatantIds.insert(combatant->id()); + if (res.second) { // combatant not already in _activeCombatantIds + _activeCombatants.push_back(combatant); + } + return res.second; + } + + std::deque> _activeCombatants; + std::unordered_set _activeCombatantIds; + /* queue[ pair( attacker, victim ) ] */ std::deque, std::shared_ptr>> _duelQueue; diff --git a/src/game/faction.cpp b/src/game/faction.cpp new file mode 100644 index 00000000..a0a786bc --- /dev/null +++ b/src/game/faction.cpp @@ -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 . + */ + +#pragma once + +#include "faction.h" +#include "../common/log.h" + +namespace reone { + +namespace game { + +const std::list HOSTILE = { + Faction::STANDARD_FACTION_HOSTILE_1, Faction::STANDARD_FACTION_HOSTILE_2 }; + +const std::list FRIENDLY = { + Faction::STANDARD_FACTION_FRIENDLY_1, Faction::STANDARD_FACTION_FRIENDLY_2 }; + +const std::list SURRENDER = { + Faction::STANDARD_FACTION_SURRENDER_1, Faction::STANDARD_FACTION_SURRENDER_2 }; + +const std::list GIZKA = { + Faction::STANDARD_FACTION_GIZKA_1, Faction::STANDARD_FACTION_GIZKA_2 }; + +std::list> groupPairs(std::list group1, std::list group2) { + std::list> aggr; + for (auto f1 : group1) { + for (auto f2 : group2) { + aggr.push_back(std::make_pair(f1, f2)); + } + } + return std::move(aggr); +} + +std::list> propagateHostileLinks() { + std::list> 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> initialize() { + std::vector> arr(MAX_NUM_FACTION, std::vector(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(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(pr.first); + size_t j = static_cast(pr.second); + + arr[i][j] = true; + arr[j][i] = true; + } + + return std::move(arr); +} + +const std::vector> _hostility = initialize(); + +bool getIsEnemy(const std::shared_ptr& oTarget, const std::shared_ptr& oSource) { + if (!oTarget || !oSource) { + debug("getIsEnemy, oTarget or OSource is nullptr"); + return false; + } + + + int s = static_cast(oSource->getFaction()); + int t = static_cast(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 diff --git a/src/game/faction.h b/src/game/faction.h new file mode 100644 index 00000000..b3af89c7 --- /dev/null +++ b/src/game/faction.h @@ -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& oTarget, const std::shared_ptr& oSource); + +} // namespace game + +} // namespace reone diff --git a/src/game/gui/mainmenu.cpp b/src/game/gui/mainmenu.cpp index 6aed4808..fdd539be 100644 --- a/src/game/gui/mainmenu.cpp +++ b/src/game/gui/mainmenu.cpp @@ -205,11 +205,13 @@ void MainMenu::onModuleSelected(const string &name) { shared_ptr 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 companion(_game->objectFactory().newCreature()); companion->load(companionCfg); + companion->setFaction(Faction::STANDARD_FACTION_FRIENDLY_1); companion->actionQueue().add(make_unique(player, 1.0f)); party.addMember(companion); diff --git a/src/game/object/creature.cpp b/src/game/object/creature.cpp index 6a566335..a6c5ac89 100644 --- a/src/game/object/creature.cpp +++ b/src/game/object/creature.cpp @@ -111,6 +111,16 @@ void Creature::load(const shared_ptr &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(blueprint->factionId()); + } } void Creature::loadAppearance(const TwoDaTable &table, int row) { diff --git a/src/game/object/creature.h b/src/game/object/creature.h index 7dee4295..7f95fb93 100644 --- a/src/game/object/creature.h +++ b/src/game/object/creature.h @@ -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> &getActiveEffects() { return _activeEffects; } void applyEffect(std::unique_ptr &&eff) { @@ -154,6 +180,7 @@ private: CombatState _cbtState = CombatState::Idle; std::deque> _activeEffects; + Faction _factionId = Faction::INVALID_STANDARD_FACTION; // END combat diff --git a/src/game/player.cpp b/src/game/player.cpp index 391dbbfe..50a1d123 100644 --- a/src/game/player.cpp +++ b/src/game/player.cpp @@ -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( - _area->combat().findNearestHostile(_party->leader()))); + _party->player()->actionQueue().add(std::make_unique(_area->combat().findNearestHostile(_party->player()))); return true; default: @@ -121,7 +121,7 @@ void Player::update(float dt) { shared_ptr partyLeader(_party->leader()); if (!partyLeader) return; - if (_party->leader()->isInterrupted()) return; + if (_party->player()->isInterrupted()) return; float heading = 0.0f; bool movement = true;