Files
cucore/src/services/combat/CombatManager.java
T
Mads Boddum ecd99ef33b Merge holocore/quality_assurance into quality_assurance
Conflicts:
	cfg/nge.cfg
	src/services/commands/CommandService.java
2016-10-14 16:21:14 +02:00

543 lines
18 KiB
Java

/************************************************************************************
* Copyright (c) 2015 /// Project SWG /// www.projectswg.com *
* *
* ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on *
* July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. *
* Our goal is to create an emulator which will provide a server for players to *
* continue playing a game similar to the one they used to play. We are basing *
* it on the final publish of the game prior to end-game events. *
* *
* This file is part of Holocore. *
* *
* -------------------------------------------------------------------------------- *
* *
* Holocore is free software: you can redistribute it and/or modify *
* it under the terms of the GNU Affero General Public License as *
* published by the Free Software Foundation, either version 3 of the *
* License, or (at your option) any later version. *
* *
* Holocore 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 Affero General Public License for more details. *
* *
* You should have received a copy of the GNU Affero General Public License *
* along with Holocore. If not, see <http://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
package services.combat;
import java.awt.Color;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import network.packets.swg.zone.object_controller.ShowFlyText;
import network.packets.swg.zone.object_controller.ShowFlyText.Scale;
import network.packets.swg.zone.object_controller.combat.CombatAction;
import intents.chat.ChatCommandIntent;
import intents.combat.CreatureKilledIntent;
import intents.combat.DeathblowIntent;
import java.util.Iterator;
import java.util.Random;
import java.util.concurrent.Future;
import resources.Posture;
import resources.PvpFaction;
import resources.PvpFlag;
import resources.combat.AttackInfoLight;
import resources.combat.AttackType;
import resources.combat.CombatStatus;
import resources.combat.HitLocation;
import resources.combat.TrailLocation;
import resources.commands.CombatCommand;
import resources.common.CRC;
import resources.common.RGB;
import resources.control.Intent;
import resources.control.Manager;
import resources.objects.SWGObject;
import resources.objects.creature.CreatureObject;
import resources.objects.creature.CreatureState;
import resources.objects.tangible.TangibleObject;
import resources.objects.weapon.WeaponObject;
import resources.server_info.Log;
import utilities.Scripts;
import utilities.ThreadUtilities;
public class CombatManager extends Manager {
private final Map<Long, CombatCreature> inCombat;
private final Set<CreatureObject> regeneratingHealthCreatures; // Only allowed outside of combat
private final Set<CreatureObject> regeneratingActionCreatures; // Always allowed
private final Map<CreatureObject, Future<?>> incapacitatedCreatures;
private final Random random;
private final CorpseService corpseService;
private final CombatXpService combatXpService;
private ScheduledExecutorService executor;
public CombatManager() {
registerForIntent(DeathblowIntent.TYPE);
inCombat = new HashMap<>();
regeneratingHealthCreatures = new HashSet<>();
regeneratingActionCreatures = new HashSet<>();
incapacitatedCreatures = new HashMap<>();
random = new Random();
corpseService = new CorpseService();
combatXpService = new CombatXpService();
addChildService(corpseService);
addChildService(combatXpService);
}
@Override
public boolean initialize() {
registerForIntent(ChatCommandIntent.TYPE);
executor = Executors.newSingleThreadScheduledExecutor(ThreadUtilities.newThreadFactory("combat-service"));
return super.initialize();
}
@Override
public boolean start() {
executor.scheduleAtFixedRate(() -> periodicChecks(), 0, 5, TimeUnit.SECONDS);
executor.scheduleAtFixedRate(() -> periodicRegeneration(), 1, 1, TimeUnit.SECONDS);
return super.start();
}
@Override
public boolean terminate() {
if (executor != null) {
executor.shutdownNow();
try {
executor.awaitTermination(3, TimeUnit.SECONDS);
} catch (InterruptedException e) {
}
}
return super.terminate();
}
@Override
public void onIntentReceived(Intent i) {
switch(i.getType()) {
case ChatCommandIntent.TYPE: processChatCommand((ChatCommandIntent) i); break;
case DeathblowIntent.TYPE: procesDeathblow((DeathblowIntent) i); break;
}
}
private void periodicChecks() {
Set<CombatCreature> removeSet = new HashSet<>();
synchronized (inCombat) {
for (CombatCreature combat : inCombat.values()) {
if (combat.getTimeSinceCombat() >= 10E3) { // 10 sec
removeSet.add(combat);
}
}
}
for (CombatCreature combat : removeSet)
removeFromCombat(combat);
}
private void removeFromCombat(CombatCreature combat) {
synchronized (inCombat) {
inCombat.remove(combat.getCreature().getObjectId());
}
exitCombat(combat.getCreature());
}
private void periodicRegeneration() {
synchronized (regeneratingActionCreatures) {
Iterator<CreatureObject> iterator = regeneratingActionCreatures.iterator();
while (iterator.hasNext()) {
regenerationActionTick(iterator.next(), iterator);
}
}
synchronized (regeneratingHealthCreatures) {
Iterator<CreatureObject> iterator = regeneratingHealthCreatures.iterator();
while (iterator.hasNext()) {
regenerationHealthTick(iterator.next(), iterator);
}
}
}
private void regenerationActionTick(CreatureObject creatureObject, Iterator<CreatureObject> iterator) {
if(creatureObject.getAction() < creatureObject.getMaxAction()) {
int modification = 13;
int level = creatureObject.getLevel();
if(level > 1) {
modification += 4 * level;
}
if(creatureObject.modifyAction(modification) == 0) {
// Their action didn't change, meaning they're maxed out
iterator.remove();
}
} else {
// Maxed out - remove 'em
iterator.remove();
}
}
private void regenerationHealthTick(CreatureObject creatureObject, Iterator<CreatureObject> iterator) {
if(creatureObject.getHealth() < creatureObject.getMaxHealth()) {
int modification = 40;
int level = creatureObject.getLevel();
if(level > 1) {
modification += 4 * level;
}
if(creatureObject.modifyHealth(modification) == 0) {
// Their health didn't change, meaning they're maxed out
iterator.remove();
}
} else {
// Maxed out - remove 'em
iterator.remove();
}
}
private void processChatCommand(ChatCommandIntent cci) {
if (!cci.getCommand().isCombatCommand() || !(cci.getCommand() instanceof CombatCommand))
return;
CombatCommand c = (CombatCommand) cci.getCommand();
CombatStatus status = canPerform(cci.getSource(), cci.getTarget(), c);
if (!handleStatus(cci.getSource(), status))
return;
Object res = Scripts.invoke("commands/combat/"+c.getName(), "doCombat", cci.getSource(), cci.getTarget(), c);
if (res == null) {
handleStatus(cci.getSource(), CombatStatus.UNKNOWN);
return;
}
updateCombatList(cci.getSource());
if (cci.getTarget() instanceof CreatureObject)
updateCombatList((CreatureObject) cci.getTarget());
if (res instanceof Number)
doCombat(cci.getSource(), cci.getTarget(), new AttackInfoLight(((Number) res).intValue()), c);
else if (res instanceof AttackInfoLight)
doCombat(cci.getSource(), cci.getTarget(), (AttackInfoLight) res, c);
else {
Log.w(this, "Unknown return from combat script: " + res);
return;
}
}
private void updateCombatList(CreatureObject creature) {
CombatCreature combat;
synchronized (inCombat) {
combat = inCombat.get(creature.getObjectId());
}
if (combat == null) {
combat = new CombatCreature(creature);
synchronized (inCombat) {
inCombat.put(creature.getObjectId(), combat);
}
}
combat.updateLastCombat();
}
private void doCombat(CreatureObject source, SWGObject target, AttackInfoLight info, CombatCommand command) {
CombatAction action = new CombatAction(source.getObjectId());
String anim = command.getRandomAnimation(source.getEquippedWeapon().getType());
action.setActionCrc(CRC.getCrc(anim));
action.setAttacker(source);
action.setClientEffectId((byte) 0);
action.setCommandCrc(command.getCrc());
action.setTrail(TrailLocation.WEAPON);
if (target instanceof CreatureObject) {
if (command.getAttackType() == AttackType.SINGLE_TARGET)
doCombatSingle(source, (CreatureObject) target, info, command);
action.addDefender((CreatureObject) target, true, (byte) 0, HitLocation.HIT_LOCATION_BODY, (short) info.getDamage());
}
source.sendObserversAndSelf(action);
}
private void doCombatSingle(CreatureObject source, CreatureObject target, AttackInfoLight info, CombatCommand command) {
if (!source.hasState(CreatureState.COMBAT))
enterCombat(source);
if (!target.hasState(CreatureState.COMBAT))
enterCombat(target);
target.addDefender(source);
source.addDefender(target);
addWeaponDamage(source, command, info);
if (target.getHealth() <= info.getDamage())
doCreatureDeath(target, source);
else
target.modifyHealth(-info.getDamage());
}
private void enterCombat(CreatureObject creature) {
creature.setStatesBitmask(CreatureState.COMBAT);
// If this creature is currently regenerating health, they should stop doing so now
synchronized(regeneratingHealthCreatures) {
regeneratingHealthCreatures.remove(creature);
}
}
private void exitCombat(CreatureObject creature) {
creature.clearStatesBitmask(CreatureState.COMBAT);
creature.clearDefenders();
// Once out of combat, we can regenerate health - unless we're dead or incapacitated!
switch(creature.getPosture()) {
case DEAD:
case INCAPACITATED:
// We can't regenerate HAM if we're incapcitated or dead
synchronized (regeneratingActionCreatures) {
regeneratingActionCreatures.remove(creature);
}
synchronized (regeneratingHealthCreatures) {
regeneratingHealthCreatures.remove(creature);
}
break;
default:
synchronized (regeneratingActionCreatures) {
regeneratingActionCreatures.add(creature);
}
synchronized (regeneratingHealthCreatures) {
regeneratingHealthCreatures.add(creature);
}
break;
}
}
private void doCreatureDeath(CreatureObject killedCreature, CreatureObject killer) {
killedCreature.setHealth(0);
killer.removeDefender(killedCreature);
// Let's check if the killer needs to remain in-combat...
if(!killer.hasDefenders()) {
// They have no active targets they're in combat with, make them exit combat
exitCombat(killer);
}
// The creature should not be able to move or turn.
killedCreature.setTurnScale(0);
killedCreature.setMovementScale(0);
// We need to handle this differently, depending on whether killedCreature is a player or not
if(killedCreature.isPlayer()) {
// TODO account for AI deathblowing players..?
// If it's a player, they need to be incapacitated
incapacitatePlayer(killedCreature);
} else {
// This is just a plain ol' NPC. Die!
killCreature(killer, killedCreature);
}
exitCombat(killedCreature);
}
private void incapacitatePlayer(CreatureObject incapacitatedPlayer) {
int incapacitationCounter = 15;
incapacitatedPlayer.setPosture(Posture.INCAPACITATED);
incapacitatedPlayer.setCounter(incapacitationCounter);
Log.i(this, "%s was incapacitated", incapacitatedPlayer);
// Once the incapacitation counter expires, revive them.
synchronized(incapacitatedCreatures) {
incapacitatedCreatures.put(incapacitatedPlayer, executor.schedule(() -> expireIncapacitation(incapacitatedPlayer), incapacitationCounter, TimeUnit.SECONDS));
}
}
private void expireIncapacitation(CreatureObject incapacitatedPlayer) {
synchronized(incapacitatedCreatures) {
incapacitatedCreatures.remove(incapacitatedPlayer);
reviveCreature(incapacitatedPlayer);
}
}
private void reviveCreature(CreatureObject revivedCreature) {
if(revivedCreature.isPlayer())
revivedCreature.setCounter(0);
revivedCreature.setPosture(Posture.UPRIGHT);
// The creature is now able to turn around and move
revivedCreature.setTurnScale(1);
revivedCreature.setMovementScale(1);
// Give 'em a percentage of their health and schedule them for HAM regeneration.
revivedCreature.setHealth((int) (revivedCreature.getBaseHealth() * 0.1)); // Restores 10% health of their base health
synchronized(regeneratingHealthCreatures) {
regeneratingHealthCreatures.add(revivedCreature);
}
synchronized(regeneratingActionCreatures) {
regeneratingActionCreatures.add(revivedCreature);
}
Log.i(this, "% was revived", revivedCreature);
}
private void killCreature(CreatureObject killer, CreatureObject corpse) {
corpse.setPosture(Posture.DEAD);
Log.i(this, "%s was killed", corpse);
new CreatureKilledIntent(killer, corpse).broadcast();
}
private void procesDeathblow(DeathblowIntent i) {
CreatureObject killer = i.getKiller();
CreatureObject corpse = i.getCorpse();
// Only deathblowing players is allowed!
if (!corpse.isPlayer()) {
return;
}
// They must be enemies
if (!corpse.isEnemy(killer)) {
return;
}
// The target of the deathblow must be incapacitated!
if (corpse.getPosture() != Posture.INCAPACITATED) {
return;
}
// If they're deathblown while incapacitated, their incapacitation expiration timer should cancel
synchronized (incapacitatedCreatures) {
Future<?> incapacitationTimer = incapacitatedCreatures.remove(corpse);
if (incapacitationTimer != null) {
if (incapacitationTimer.cancel(false)) { // If the task is running, let them get back up
killCreature(killer, corpse);
Log.i(this, "%s was deathblown by %s", corpse, killer);
}
} else {
// Can't happen with the current code, but in case it's ever refactored...
Log.e(this, "Incapacitation timer for player %s being deathblown unexpectedly didn't exist!", "");
}
}
}
private boolean handleStatus(CreatureObject source, CombatStatus status) {
switch (status) {
case SUCCESS:
return true;
case NO_TARGET:
showFlyText(source, "@combat_effects:target_invalid_fly", Scale.MEDIUM, Color.WHITE, ShowFlyText.Flag.PRIVATE);
return false;
case TOO_FAR:
showFlyText(source, "@combat_effects:range_too_far", Scale.MEDIUM, Color.CYAN, ShowFlyText.Flag.PRIVATE);
return false;
default:
showFlyText(source, "@combat_effects:cant_attack_fly", Scale.MEDIUM, Color.WHITE, ShowFlyText.Flag.PRIVATE);
Log.e(this, "Character unable to attack. Player: %s Reason: %s", source.getName(), status);
return false;
}
}
private CombatStatus canPerform(CreatureObject source, SWGObject target, CombatCommand c) {
if (source.getEquippedWeapon() == null)
return CombatStatus.NO_WEAPON;
if (!(target instanceof TangibleObject))
return CombatStatus.INVALID_TARGET;
TangibleObject tangibleTarget = (TangibleObject) target;
if(tangibleTarget.getPvpFaction() != PvpFaction.NEUTRAL) {
if(!tangibleTarget.isEnemy(source)) {
return CombatStatus.INVALID_TARGET;
}
} else if ((tangibleTarget.getPvpFlags() & PvpFlag.ATTACKABLE.getBitmask()) == 0)
return CombatStatus.INVALID_TARGET;
if(target instanceof CreatureObject) {
CreatureObject creature = (CreatureObject) target;
switch(creature.getPosture()) {
case DEAD:
case INCAPACITATED:
return CombatStatus.INVALID_TARGET;
}
}
CombatStatus status;
switch (c.getAttackType()) {
case AREA:
case TARGET_AREA:
status = canPerformArea(source, c);
break;
case SINGLE_TARGET:
status = canPerformSingle(source, target, c);
break;
default:
status = CombatStatus.UNKNOWN;
break;
}
if (status != CombatStatus.SUCCESS)
return status;
status = Scripts.invoke("commands/combat/"+c.getName(), "canPerform", source, target, c);
if (status == null) {
return CombatStatus.UNKNOWN;
}
return status;
}
private CombatStatus canPerformSingle(CreatureObject source, SWGObject target, CombatCommand c) {
if (target == null || !(target instanceof CreatureObject))
return CombatStatus.NO_TARGET;
WeaponObject weapon = source.getEquippedWeapon();
double dist = source.getLocation().distanceTo(target.getLocation());
if (dist > weapon.getMaxRange() || (dist > c.getMaxRange() && c.getMaxRange() > 0))
return CombatStatus.TOO_FAR;
return CombatStatus.SUCCESS;
}
private CombatStatus canPerformArea(CreatureObject source, CombatCommand c) {
return CombatStatus.SUCCESS;
}
private void addWeaponDamage(CreatureObject source, CombatCommand command, AttackInfoLight info) {
int abilityDamage = info.getDamage();
WeaponObject weapon = source.getEquippedWeapon();
int minDamage = weapon.getMinDamage();
int weaponDamage = random.nextInt((weapon.getMaxDamage() - minDamage) + 1) + minDamage;
weaponDamage *= command.getPercentAddFromWeapon();
info.setDamage(abilityDamage + weaponDamage);
}
private void showFlyText(TangibleObject obj, String text, Scale scale, Color c, ShowFlyText.Flag ... flags) {
obj.sendSelf(new ShowFlyText(obj.getObjectId(), text, scale, new RGB(c), flags));
}
private static class CombatCreature {
private final CreatureObject creature;
private long lastCombat;
public CombatCreature(CreatureObject creature) {
this.creature = creature;
}
public void updateLastCombat() {
lastCombat = System.nanoTime();
}
public double getTimeSinceCombat() {
return (System.nanoTime() - lastCombat) / 1E6;
}
public CreatureObject getCreature() {
return creature;
}
}
}