package com.planet_ink.coffee_mud.Abilities.Skills; import com.planet_ink.coffee_mud.core.interfaces.*; import com.planet_ink.coffee_mud.core.*; import com.planet_ink.coffee_mud.core.collections.*; import com.planet_ink.coffee_mud.Abilities.interfaces.*; import com.planet_ink.coffee_mud.Areas.interfaces.*; import com.planet_ink.coffee_mud.Behaviors.interfaces.*; import com.planet_ink.coffee_mud.CharClasses.interfaces.*; import com.planet_ink.coffee_mud.Commands.interfaces.*; import com.planet_ink.coffee_mud.Common.interfaces.*; import com.planet_ink.coffee_mud.Exits.interfaces.*; import com.planet_ink.coffee_mud.Items.interfaces.*; import com.planet_ink.coffee_mud.Libraries.interfaces.*; import com.planet_ink.coffee_mud.Locales.interfaces.*; import com.planet_ink.coffee_mud.MOBS.interfaces.*; import com.planet_ink.coffee_mud.Races.interfaces.*; import java.util.*; import java.util.concurrent.TimeUnit; /* Copyright 2017-2019 Bo Zimmerman Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ public class Skill_CombatLog extends StdSkill { @Override public String ID() { return "Skill_CombatLog"; } private final static String localizedName = CMLib.lang().L("Combat Log"); @Override public String name() { return localizedName; } private final static String localizedStaticDisplay = CMLib.lang().L("(Combat Logging: "); @Override public String displayText() { if(loggingM != null) return localizedStaticDisplay + loggingM.name() + ")"; else return ""; } @Override protected int canAffectCode() { return CAN_MOBS; } @Override protected int canTargetCode() { return CAN_MOBS; } @Override public int abstractQuality() { return Ability.QUALITY_INDIFFERENT; } private static final String[] triggerStrings = I(new String[] { "COMBATLOG" }); @Override public String[] triggerStrings() { return triggerStrings; } @Override public int classificationCode() { return Ability.ACODE_SKILL | Ability.DOMAIN_COMBATLORE; } @Override public int maxRange() { return adjustedMaxInvokerRange(20); } @Override public int usageType() { return USAGE_MANA; } protected Map<CombatStat, long[]> stats = new Hashtable<CombatStat, long[]>(); protected Map<Faction, long[]> factionChanges = new Hashtable<Faction, long[]>(); protected MOB loggingM = null; protected long secondsPerTick = (CMProps.getTickMillis() / 1000); protected volatile boolean wasInCombat = false; protected volatile int numCombatants = 0; protected volatile int numManaLastTick = 0; protected volatile int numMovesLastTick= 0; protected enum CombatStat { NUM_COMBATS, NUM_COMBAT_ROUNDS, NUM_ENEMIES_FOUGHT, NUM_ENEMIES_KILLED, NUM_XP_GAINED, NUM_GOLD_LOOTED, NUM_SECONDS_TOTAL, NUM_SECONDS_COMBAT, NUM_MANA_USED, NUM_MANA_USED_COMBAT, NUM_MOVEMENT_USED, NUM_MOVEMENT_USED_COMBAT, NUM_DAMAGE_DONE, NUM_DAMAGE_TAKEN, NUM_HEALING_DONE, } @Override public boolean okMessage(final Environmental myHost, final CMMsg msg) { if(!(affected instanceof MOB)) return true; return true; } protected String getReport(final Session session, final MOB mob, final int level) { final StringBuilder rep = new StringBuilder(""); rep.append(L("\n\r^HCombat statistics for ^w@x1 \n\r",mob.name())); if(stats.size() == CombatStat.values().length) { final Map<CombatStat,long[]> stats = new Hashtable<CombatStat,long[]>(); stats.putAll(this.stats); final TimeClock clock = CMLib.time().localClock(mob); final String totalLogTime = clock.deriveEllapsedTimeString(stats.get(CombatStat.NUM_SECONDS_TOTAL)[0]*1000); rep.append(L("^HTotal Log Time: ^w@x1\n\r",totalLogTime)); final String totalCombatTime = clock.deriveEllapsedTimeString(stats.get(CombatStat.NUM_SECONDS_COMBAT)[0]*1000); rep.append(L("^HTotal Combat Time: ^w@x1\n\r",totalCombatTime)); final int colWidth = CMLib.lister().fixColWidth(26, session); final int colWidth1 = colWidth - (int)Math.round(CMath.mul(colWidth, .20)); final int colWidth2 = colWidth + (int)Math.round(CMath.mul(colWidth, .09)); rep.append("\n\r"); rep.append(CMStrings.padRight(L("^HEnemies defeated: ^w@x1",""+stats.get(CombatStat.NUM_ENEMIES_KILLED)[0]),colWidth1)); if((level>=5)&&(stats.get(CombatStat.NUM_COMBATS)[0]>0)) { final double npc = Math.round(stats.get(CombatStat.NUM_ENEMIES_KILLED)[0]/stats.get(CombatStat.NUM_COMBATS)[0]); rep.append(L("^HEnemies per Combat: ^w@x1",""+npc)); rep.append("\n\r"); } rep.append("\n\r"); if(level>=4) { rep.append(CMStrings.padRight(L("^HGold gained : ^w@x1",""+stats.get(CombatStat.NUM_GOLD_LOOTED)[0]),colWidth1)); if((level>=9)&&(stats.get(CombatStat.NUM_ENEMIES_KILLED)[0]>0)) { final double age = Math.round(stats.get(CombatStat.NUM_GOLD_LOOTED)[0]/stats.get(CombatStat.NUM_ENEMIES_KILLED)[0]); rep.append(CMStrings.padRight(L("^HGold per enemy : ^w@x1",""+age),colWidth2)); if((level>=10)&&(stats.get(CombatStat.NUM_SECONDS_TOTAL)[0]>(CMProps.getMillisPerMudHour()/1000))) { final double agh = Math.round(stats.get(CombatStat.NUM_GOLD_LOOTED)[0]/((stats.get(CombatStat.NUM_SECONDS_TOTAL)[0]*1000)/CMProps.getMillisPerMudHour())); rep.append(L("^HGold per game-hour: ^w@x1",""+agh)); } } rep.append("\n\r"); } if(level>=4) { rep.append(CMStrings.padRight(L("^HExp. gained : ^w@x1",""+stats.get(CombatStat.NUM_XP_GAINED)[0]),colWidth1)); if((level>=9)&&(stats.get(CombatStat.NUM_ENEMIES_KILLED)[0]>0)) { final double age = Math.round(stats.get(CombatStat.NUM_XP_GAINED)[0]/stats.get(CombatStat.NUM_ENEMIES_KILLED)[0]); rep.append(CMStrings.padRight(L("^HExp. per enemy : ^w@x1",""+age),colWidth2)); if((level>=10)&&(stats.get(CombatStat.NUM_SECONDS_TOTAL)[0]>(CMProps.getMillisPerMudHour()/1000))) { final double agh = Math.round(stats.get(CombatStat.NUM_XP_GAINED)[0]/((stats.get(CombatStat.NUM_SECONDS_TOTAL)[0]*1000)/CMProps.getMillisPerMudHour())); rep.append(L("^HExp. per game-hour: ^w@x1",""+agh)); } } rep.append("\n\r"); } rep.append(CMStrings.padRight(L("^HDamage done : ^w@x1",""+stats.get(CombatStat.NUM_DAMAGE_DONE)[0]),colWidth1)); if((level >= 2)&&(stats.get(CombatStat.NUM_COMBATS)[0]>0)) { final double adc = Math.round(stats.get(CombatStat.NUM_DAMAGE_DONE)[0]/stats.get(CombatStat.NUM_COMBATS)[0]); rep.append(CMStrings.padRight(L("^HDamage per combat : ^w@x1",""+adc),colWidth2)); if((level>=6)&&(stats.get(CombatStat.NUM_SECONDS_TOTAL)[0]>0)) { final double ads = Math.round(stats.get(CombatStat.NUM_DAMAGE_DONE)[0]/stats.get(CombatStat.NUM_SECONDS_TOTAL)[0]); rep.append(L("^HDamage per second : ^w@x1",""+ads)); } } rep.append("\n\r"); rep.append(CMStrings.padRight(L("^HHealing done: ^w@x1",""+stats.get(CombatStat.NUM_HEALING_DONE)[0]),colWidth1)); if((level >= 2)&&(stats.get(CombatStat.NUM_COMBATS)[0]>0)) { final double adc = Math.round(stats.get(CombatStat.NUM_HEALING_DONE)[0]/stats.get(CombatStat.NUM_COMBATS)[0]); rep.append(CMStrings.padRight(L("^HHealing per combat: ^w@x1",""+adc),colWidth2)); if((level>=6)&&(stats.get(CombatStat.NUM_SECONDS_TOTAL)[0]>0)) { final double ads = Math.round(stats.get(CombatStat.NUM_HEALING_DONE)[0]/stats.get(CombatStat.NUM_SECONDS_TOTAL)[0]); rep.append(L("^HHealing per second: ^w@x1",""+ads)); } } rep.append("\n\r"); rep.append(CMStrings.padRight(L("^HDamage taken: ^w@x1",""+stats.get(CombatStat.NUM_DAMAGE_TAKEN)[0]),colWidth1)); if((level >= 2)&&(stats.get(CombatStat.NUM_COMBATS)[0]>0)) { final double adc = Math.round(stats.get(CombatStat.NUM_DAMAGE_TAKEN)[0]/stats.get(CombatStat.NUM_COMBATS)[0]); rep.append(CMStrings.padRight(L("^HDmg taken/combat : ^w@x1",""+adc),colWidth2)); if((level>=6)&&(stats.get(CombatStat.NUM_SECONDS_TOTAL)[0]>0)) { final double ads = Math.round(stats.get(CombatStat.NUM_DAMAGE_TAKEN)[0]/stats.get(CombatStat.NUM_SECONDS_TOTAL)[0]); rep.append(L("^HDmg taken/second : ^w@x1",""+ads)); } } rep.append("\n\r"); if(level >= 1) { rep.append(CMStrings.padRight(L("^HMana used : ^w@x1",""+stats.get(CombatStat.NUM_MANA_USED)[0]),colWidth1)); if((level >= 3)&&(stats.get(CombatStat.NUM_COMBATS)[0]>0)) { final double muc = Math.round(stats.get(CombatStat.NUM_MANA_USED_COMBAT)[0]/stats.get(CombatStat.NUM_COMBATS)[0]); rep.append(CMStrings.padRight(L("^HMana used/combat : ^w@x1",""+muc),colWidth2)); if((level>=7)&&(stats.get(CombatStat.NUM_SECONDS_TOTAL)[0]>0)) { final double mus = Math.round(stats.get(CombatStat.NUM_MANA_USED_COMBAT)[0]/stats.get(CombatStat.NUM_SECONDS_TOTAL)[0]); rep.append(L("^HMana used/second : ^w@x1",""+mus)); } } rep.append("\n\r"); } if(level >= 1) { rep.append(CMStrings.padRight(L("^HMoves used : ^w@x1",""+stats.get(CombatStat.NUM_MOVEMENT_USED)[0]),colWidth1)); if((level >= 3)&&(stats.get(CombatStat.NUM_COMBATS)[0]>0)) { final double muc = Math.round(stats.get(CombatStat.NUM_MOVEMENT_USED_COMBAT)[0]/stats.get(CombatStat.NUM_COMBATS)[0]); rep.append(CMStrings.padRight(L("^HMoves used/combat : ^w@x1",""+muc),colWidth2)); if((level>=7)&&(stats.get(CombatStat.NUM_SECONDS_TOTAL)[0]>0)) { final double mus = Math.round(stats.get(CombatStat.NUM_MOVEMENT_USED_COMBAT)[0]/stats.get(CombatStat.NUM_SECONDS_TOTAL)[0]); rep.append(L("^HMoves used/second : ^w@x1",""+mus)); } } rep.append("\n\r"); } if(level>=5) { rep.append("\n\r"); for(final Faction F : factionChanges.keySet()) { final long[] val = factionChanges.get(F); if(val[0]>0) rep.append(L("^H@x1 gained: ^w@x2\n\r",F.name(),""+val[0])); if(val[1]<0) rep.append(L("^H@x1 lost: ^w@x2\n\r",F.name(),""+val[1])); } } rep.append("\n\r"); } return rep.toString(); } @Override public void executeMsg(final Environmental myHost, final CMMsg msg) { if((msg.sourceMinor()==CMMsg.TYP_DEATH) &&(msg.tool()==affected) &&(msg.tool() instanceof MOB)) { if(stats.size() == CombatStat.values().length) stats.get(CombatStat.NUM_ENEMIES_KILLED)[0]++; } if(msg.source()==affected) { switch(msg.targetMinor()) { case CMMsg.TYP_DAMAGE: if(msg.target() instanceof MOB) { if(stats.size() == CombatStat.values().length) stats.get(CombatStat.NUM_DAMAGE_DONE)[0]+=msg.value(); } break; case CMMsg.TYP_HEALING: if(msg.target() instanceof MOB) { if(stats.size() == CombatStat.values().length) stats.get(CombatStat.NUM_HEALING_DONE)[0]+=msg.value(); } break; } switch(msg.sourceMinor()) { case CMMsg.TYP_EXPCHANGE: if(stats.size() == CombatStat.values().length) stats.get(CombatStat.NUM_XP_GAINED)[0]+=msg.value(); break; case CMMsg.TYP_GET: if((msg.tool() instanceof Coins) &&(msg.target() instanceof DeadBody)) { if(stats.size() == CombatStat.values().length) stats.get(CombatStat.NUM_GOLD_LOOTED)[0]+=Math.round(((Coins)msg.tool()).getTotalValue()); } break; case CMMsg.TYP_FACTIONCHANGE: if (msg.othersMessage() != null) { if ((msg.value() < Integer.MAX_VALUE) && (msg.value() > Integer.MIN_VALUE)) { final Faction F=CMLib.factions().getFaction(msg.othersMessage()); if(F!=null) { if(!factionChanges.containsKey(F)) factionChanges.put(F, new long[]{0,0}); if(msg.value()>0) factionChanges.get(F)[0]+=msg.value(); else if(msg.value()<0) factionChanges.get(F)[1]-=msg.value(); } } } break; } } else if((msg.target()==affected) &&(msg.targetMinor()==CMMsg.TYP_DAMAGE)) { if(stats.size() == CombatStat.values().length) stats.get(CombatStat.NUM_DAMAGE_TAKEN)[0]+=msg.value(); } } @Override public boolean tick(final Tickable ticking, final int tickID) { if(stats.size()==0) { for(final CombatStat stat : CombatStat.values()) stats.put(stat, new long[]{0}); secondsPerTick = (CMProps.getTickMillis()/1000); } stats.get(CombatStat.NUM_SECONDS_TOTAL)[0]+=secondsPerTick; final Physical affected=this.affected; if(affected instanceof MOB) { final MOB mob=(MOB)affected; int manaChange=0; if(mob.curState().getMana()<this.numManaLastTick) manaChange=(this.numManaLastTick-mob.curState().getMana()); int moveChange=0; if(mob.curState().getMovement()<this.numMovesLastTick) moveChange=(this.numMovesLastTick-mob.curState().getMovement()); stats.get(CombatStat.NUM_MANA_USED)[0]+=manaChange; stats.get(CombatStat.NUM_MOVEMENT_USED)[0]+=moveChange; if(mob.isInCombat()) { stats.get(CombatStat.NUM_MANA_USED_COMBAT)[0]+=manaChange; stats.get(CombatStat.NUM_MOVEMENT_USED_COMBAT)[0]+=moveChange; stats.get(CombatStat.NUM_SECONDS_COMBAT)[0]+=secondsPerTick; if(!this.wasInCombat) { this.wasInCombat=true; stats.get(CombatStat.NUM_COMBATS)[0]++; this.numCombatants=0; } final Room R=mob.location(); if(R!=null) { int numCounted=0; for(final Enumeration<MOB> r=R.inhabitants();r.hasMoreElements();) { final MOB M=r.nextElement(); if((M!=mob) &&(M.getVictim()==mob)) numCounted++; } if(numCounted > this.numCombatants) this.numCombatants=numCounted; } stats.get(CombatStat.NUM_COMBAT_ROUNDS)[0]++; } else { if(this.wasInCombat) { this.wasInCombat=false; stats.get(CombatStat.NUM_ENEMIES_FOUGHT)[0]+=this.numCombatants; } } this.numManaLastTick=mob.curState().getMana(); this.numMovesLastTick=mob.curState().getMana(); } return super.tick(ticking, tickID); } @Override public void unInvoke() { super.unInvoke(); } @Override protected boolean ignoreCompounding() { return true; } @Override public boolean invoke(final MOB mob, final List<String> commands, final Physical givenTarget, final boolean auto, final int asLevel) { final boolean logging = (this.loggingM != null) && (this.loggingM.fetchEffect(ID()) != null); if(commands.size()==0) { if(!logging) mob.tell(L("Combat log whom?")); else mob.tell(L("Do what? STOP to stop logging. REPORT to announce your current log. WRITE <target> to write it down.")); return false; } final Room R=mob.location(); if(R==null) return false; final MOB target; if(!logging) { target=this.getTarget(mob,commands,givenTarget); if(target==null) return false; } else { final String cmd=commands.get(0).toUpperCase().trim(); if("STOP".startsWith(cmd)) { if(this.loggingM != null) { final String name=this.loggingM.name(mob); final Ability A=this.loggingM.fetchEffect(ID()); if(A!=null) A.unInvoke(); this.loggingM = null; mob.tell(L("You stop combat logging on @x1.",name)); } return true; } else if("REPORT".startsWith(cmd)) { CMLib.commands().postSay(mob, this.getReport(mob.session(), loggingM, super.getXLEVELLevel(mob))); return true; } else if("WRITE".startsWith(cmd)) { final Skill_Write write=(Skill_Write)mob.fetchAbility("Skill_Write"); if(write == null) { mob.tell(L("You don't know how to write!")); return false; } if(commands.size()<2) { mob.tell(L("Write the report on what?")); return false; } final String onWhat=CMParms.combine(commands,1); final List<String> writeParms=new ArrayList<String>(); writeParms.add(onWhat); writeParms.add(this.getReport(null, loggingM, super.getXLEVELLevel(mob))); return write.invoke(mob, writeParms, null, auto, asLevel); } else { mob.tell(L("'@x1' is an unknown command while logging. Try STOP, REPORT, or WRITE <target>.",cmd)); return false; } } if(!super.invoke(mob,commands,givenTarget,auto,asLevel)) return false; final boolean success=proficiencyCheck(mob,0,auto); if(success) { final CMMsg msg=CMClass.getMsg(mob,target,this,CMMsg.MASK_EYES|CMMsg.TYP_OK_VISUAL|(auto?CMMsg.MASK_ALWAYS:0),L("<S-NAME> start(s) watching <T-YOUPOSS> combat maneuvers.")); if(R.okMessage(mob,msg)) { R.send(mob,msg); final Skill_CombatLog log = (Skill_CombatLog)super.beneficialAffect(mob,target,asLevel,Integer.MAX_VALUE/2); if(log != null) { log.stats = this.stats; log.factionChanges = this.factionChanges; this.stats.clear(); this.factionChanges.clear(); log.makeLongLasting(); this.loggingM = target; } } } else return beneficialVisualFizzle(mob,target,L("<S-NAME> attempt(s) to start combat logging on <T-NAMESELF>, but fail(s).")); return success; } }