-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathCombatSim.py
More file actions
303 lines (252 loc) · 14.9 KB
/
CombatSim.py
File metadata and controls
303 lines (252 loc) · 14.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
from Characters.Jingyuan import Jingyuan
from Characters.Sparkle import Sparkle
from Characters.Tingyun import Tingyun
from Characters.Gallagher import Gallagher
from Characters.HuoHuo import HuoHuo
from Characters.Sunday import Sunday
from Characters.Jade import Jade
from RelicStats import RelicStats
from HelperFuncs import *
from Misc import *
from Enemy import *
cycles = 5 # comment out this line if running the simulator from an external script
log = True
manual = False
# noinspection PyUnboundLocalVariable,PyUnusedLocal
def startSimulator(cycleLimit = 5, s1: Character = None, s2: Character = None, s3: Character = None, s4: Character = None, outputLog: bool = False, enemyModule = None, manualMode = False) -> str:
# =============== SETTINGS ===============
# Enemy Settings
numEnemies = 2
enemyLevel = [95, 95] # make sure that the number of entries in this list is the same as "numEnemies"
enemyTypes = [EnemyType.BOSS, EnemyType.ELITE] # make sure that the number of entries in this list is the same as "numEnemies"
enemySPD = [158.4, 145.2] # make sure that the number of entries in this list is the same as "numEnemies"
toughness = [160, 100] # make sure that the number of entries in this list is the same as "numEnemies"
attackRatio = atkRatio # from Misc.py
weaknesses = [Element.LIGHTNING]
actionOrder = [1, 1, 2] # determines how many attacks enemies will have per turn
enemyModule = enemyModule if enemyModule else EnemyModule(numEnemies, enemyLevel, enemyTypes, enemySPD, toughness, attackRatio, weaknesses, actionOrder)
fastJY = RelicStats(6, 2, 0, 2, 4, 0, 4, 4, 4, 4, 7, 11, Pwr.CR_PERCENT, Pwr.SPD, Pwr.DMG_PERCENT, Pwr.ATK_PERCENT)
jadeJY = RelicStats(6, 2, 0, 2, 4, 0, 4, 4, 4, 4, 7, 11, Pwr.CR_PERCENT, Pwr.ATK_PERCENT, Pwr.DMG_PERCENT, Pwr.ATK_PERCENT)
baseJY = RelicStats(0, 2, 0, 2, 4, 0, 4, 4, 4, 4, 13, 11, Pwr.CR_PERCENT, Pwr.ATK_PERCENT, Pwr.DMG_PERCENT, Pwr.ATK_PERCENT)
sparkSunJY = RelicStats(0, 2, 0, 2, 4, 0, 4, 4, 4, 4, 7, 17, Pwr.CR_PERCENT, Pwr.ATK_PERCENT, Pwr.DMG_PERCENT, Pwr.ATK_PERCENT)
fastDay = RelicStats(14, 4, 0, 4, 4, 0, 4, 4, 4, 4, 0, 6, Pwr.CD_PERCENT, Pwr.SPD, Pwr.DEF_PERCENT, Pwr.ERR_PERCENT)
slowDay = RelicStats(4, 4, 0, 4, 4, 0, 4, 4, 4, 10, 0, 10, Pwr.CD_PERCENT, Pwr.SPD, Pwr.DEF_PERCENT, Pwr.ERR_PERCENT)
# Character Settings
if all([a is None for a in [s1, s2, s3, s4]]):
slot1 = Jingyuan(0, Role.DPS, 2, eidolon=0, targetPrio=Priority.DEFAULT, subs=baseJY)
slot2 = Sunday(1, Role.SUP1, 2, eidolon=0, targetPrio=Priority.DEFAULT, targetRole=Role.DPS, subs=fastDay)
slot3 = Sparkle(2, Role.SUP2, 2, eidolon=0, targetPrio=Priority.DEFAULT, targetRole=Role.DPS)
slot4 = HuoHuo(3, Role.SUS, 2, eidolon=0, targetPrio=Priority.DEFAULT)
# Simulation Settings
totalEnemyAttacks = 0
logLevel = logging.DEBUG
# CRITICAL: Only prints the main action taken during each turn + ultimates
# WARNING: Prints the above plus details on all actions recorded during the turn (FuA/Bonus attacks etc.), and all AV adjustments
# INFO: Prints the above plus buff and debuff expiry, speed adjustments, av of all chars at the start of each turn
# DEBUG: Prints the above plus all associated buffs and debuffs present during each turn
# =============== END OF SETTINGS ===============
# Logging Config
if not s1:
playerTeam = [slot1, slot2, slot3, slot4]
else:
playerTeam = [s1, s2, s3, s4]
if outputLog:
log_folder = "Output"
teamInfo = "".join([char.name for char in playerTeam])
enemyInfo = f"_{enemyModule.numEnemies}Enemies_{cycleLimit}Cycles"
logging.basicConfig(
filename=f"{log_folder}/{teamInfo}{enemyInfo}.log",
level=logLevel,
format="%(message)s",
filemode="w"
)
else:
logging.disable(logging.CRITICAL) # Disable all logging messages
avLimit = cycleLimit * 100 + 50
simAV = 0
# Damage Module
dmg = DmgTracker()
# Skill Point Module
startingSP = 3 + [p.relic1.name for p in playerTeam if p.relic1.setType == 4].count("Passerby of Wandering Cloud")
maxSP = (8 if findCharName(playerTeam, "Sparkle").eidolon >= 4 else 7) if inTeam(playerTeam, "Sparkle") else 5
spTracker = SpTracker(startingSP, maxSP)
# Summons
summons = addSummons([p for p in playerTeam if p.hasSummon])
# Print Enemy Info
eTeam = []
for i in range(enemyModule.numEnemies):
adjList = []
if (i - 1) >= 0:
adjList.append(i - 1)
if (i + 1) < enemyModule.numEnemies:
adjList.append(i + 1)
eLevel = enemyModule.enemyLevel[i]
eType = enemyModule.enemyTypes[i]
eSPD = enemyModule.enemySPD[i]
eToughness = enemyModule.toughness[i]
eAction = enemyModule.actionOrder
eWeaknesses = enemyModule.weaknesses
eTeam.append(Enemy(i, eLevel, eType, eSPD, eToughness, eAction, eWeaknesses, adjList))
manualPrint(manualMode, "===============================================================================================================================================================")
logging.critical("Enemy Team:")
manualPrint(manualMode, "Enemy Team:")
for enemy in eTeam:
logging.critical(enemy)
manualPrint(manualMode, enemy)
# Print Char Info
logging.critical("\nPlayer Team:")
manualPrint(manualMode, "\nPlayer Team:")
for char in playerTeam:
logging.critical(f"{char}\n")
manualPrint(manualMode, f"{char}\n")
# Setup equipment and char traces
teamBuffs, enemyDebuffs, advList, delayList = [], [], [], []
for char in playerTeam:
initBuffs, initDebuffs, initAdv, initDelay = char.equip()
teamBuffs, enemyDebuffs, advList, delayList = handleAdditions(playerTeam, eTeam, teamBuffs, enemyDebuffs, advList, delayList, initBuffs, initDebuffs, initAdv, initDelay)
# Setup initial AV
for char in playerTeam:
initCharAV(char, teamBuffs) # apply any pre-existing speed buffs
logging.warning("\nInitial AV Adjustments")
avAdjustment(playerTeam, advList) # apply any "on battle start" advances
advList = [] # clear advList after applying
logging.warning("\nInitial Enemy Delays")
delayList = delayAdjustment(eTeam, delayList, enemyDebuffs) # apply any "on battle start" delays
allUnits = sortUnits(playerTeam + eTeam + summons)
setPriority(allUnits)
# Simulator Loop
logging.critical("\n==========COMBAT SIMULATION STARTED==========")
while simAV < avLimit:
unit = allUnits[0] # Find next turn
av = unit.currAV
simAV += av
turnList = []
if simAV > avLimit: # don't parse turn once over avLimit
break
logging.critical("\n<<< NEW TURN >>>")
# Reduce AV of all chars
for u in allUnits:
u.standardAVred(av)
logging.info(f"- {u.name} AV: {u.currAV:.3f} ENERGY: {u.currEnergy if u.isChar() and not u.isSummon() else 0:.3f}")
logging.info("")
# Apply any special effects
teamBuffs, enemyDebuffs, advList, delayList = handleSpecialEffects(unit, playerTeam, summons, eTeam, teamBuffs, enemyDebuffs, advList, delayList, "START", spTracker, dmg, manualMode=manualMode)
if unit.isChar() and not unit.isSummon():
# Check if any unit can ult
teamBuffs, enemyDebuffs, advList, delayList = handleUlts(playerTeam, summons, eTeam, teamBuffs, enemyDebuffs, advList, delayList, spTracker, dmg, manualMode=manualMode, simAV=simAV)
# Handle unit Turns
if not unit.isChar(): # Enemy turn
numAttacks = unit.takeTurn()
totalEnemyAttacks += numAttacks
action = f"ACTION > [ENEMY] TotalAV: {simAV:.3f} | TurnAV: {av:.3f} | {unit.name} | {numAttacks} attacks"
logging.critical(action)
manualPrint(manualMode, action)
for i in range(numAttacks):
for char in playerTeam:
bl, dbl, al, dl, tl = char.useHit(unit.enemyID)
teamBuffs, enemyDebuffs, advList, delayList = handleAdditions(playerTeam, eTeam, teamBuffs, enemyDebuffs, advList, delayList, bl, dbl, al, dl)
turnList.extend(tl)
energyList = addEnergy(playerTeam, eTeam, numAttacks, enemyModule.attackRatios, teamBuffs) # might be useful someday lol
energyMsg = " CharEnergy -"
for i in range(4):
energyMsg += f" {playerTeam[i].name}: Hit {energyList[i] * 10:.3f} Total: {playerTeam[i].currEnergy:.3f} |"
logging.warning(energyMsg)
dmg.addDebuffDMG(takeDebuffDMG(unit, playerTeam, teamBuffs, enemyDebuffs))
elif unit.isChar() and not unit.isSummon(): # Character Turn
if manualMode:
moveType, target = manualModule(spTracker, playerTeam, summons, eTeam, simAV, unit, "TURN")
else:
moveType, target = unit.takeTurn(), unit.defaultTarget
action = f"ACTION > [CHAR] TotalAV: {simAV:.3f} | TurnAV: {av:.3f} | {unit.name} | {moveType}-move"
logging.critical(action)
manualPrint(manualMode, action)
teamBuffs = tickBuffs(unit, teamBuffs, "START")
if moveType == "E":
bl, dbl, al, dl, tl = unit.useSkl(target)
elif moveType == "A":
bl, dbl, al, dl, tl = unit.useBsc(target)
else:
manualPrint(manualMode, "Invalid move type!")
bl, dbl, al, dl, tl = [], [], [], [], []
teamBuffs, enemyDebuffs, advList, delayList = handleAdditions(playerTeam, eTeam, teamBuffs, enemyDebuffs, advList, delayList, bl, dbl, al, dl)
turnList.extend(tl)
elif unit.isChar() and unit.isSummon():
action = f"ACTION > [SUMMON] TotalAV: {simAV:.3f} | TurnAV: {av:.3f} | {unit.name}"
logging.critical(action) # Summon logic
manualPrint(manualMode, action)
bl, dbl, al, dl, tl = unit.takeTurn()
teamBuffs, enemyDebuffs, advList, delayList = handleAdditions(playerTeam, eTeam, teamBuffs, enemyDebuffs, advList, delayList, bl, dbl, al, dl)
turnList.extend(tl)
# Handle any pending attacks:
teamBuffs, enemyDebuffs, advList, delayList, turnList = processTurnList(turnList, playerTeam, summons, eTeam, teamBuffs, enemyDebuffs, advList, delayList, spTracker, dmg, manualMode=manualMode)
# Handle any errGain from unit turns
teamBuffs = handleEnergyFromBuffs(teamBuffs, enemyDebuffs, playerTeam, eTeam)
# Check if any unit can ult
teamBuffs, enemyDebuffs, advList, delayList = handleUlts(playerTeam, summons, eTeam, teamBuffs, enemyDebuffs, advList, delayList, spTracker, dmg, manualMode=manualMode, simAV=simAV)
if unit.isChar() and not unit.isSummon():
teamBuffs = tickBuffs(unit, teamBuffs, "END") # THIS MARKS THE END OF THE PLAYER TURN
elif not unit.isChar():
enemyDebuffs = tickDebuffs(unit, enemyDebuffs) # THIS MARKS THE END OF THE ENEMY TURN
# Apply any special effects
teamBuffs, enemyDebuffs, advList, delayList = handleSpecialEffects(unit, playerTeam, summons, eTeam, teamBuffs, enemyDebuffs, advList, delayList, "END", spTracker, dmg, manualMode=manualMode)
# Check if any unit can ult
teamBuffs, enemyDebuffs, advList, delayList = handleUlts(playerTeam, summons, eTeam, teamBuffs, enemyDebuffs, advList, delayList, spTracker, dmg, manualMode=manualMode, simAV=simAV)
# Apply any speed adjustments
spdAdjustment(playerTeam, teamBuffs)
enemySPDAdjustment(eTeam, enemyDebuffs)
# Reset the AV of the current unit by checking its current speed
if not unit.isChar() or not unit.isSummon():
resetUnitAV(unit, teamBuffs, enemyDebuffs)
avLog = f"AV > {unit.name} AV reset to {unit.currAV:.3f} | {unit.currSPD:.3f} SPD"
logging.warning(avLog)
manualPrint(manualMode, avLog)
allUnits = sortUnits(allUnits)
# Reset priorities
setPriority(allUnits)
# Apply any enemy delays
delayList = delayAdjustment(eTeam, delayList, enemyDebuffs)
# Apply any character/summon AV adjustments
avAdjustment(playerTeam + summons, advList)
advList = []
if unit.isChar() and unit.isSummon():
resetUnitAV(unit, [], []) # summons cannot be advanced during their own turn
avLog = f"AV > {unit.name} AV reset to {unit.currAV:.3f} | {unit.currSPD:.3f} SPD"
logging.warning(avLog)
manualPrint(manualMode, avLog)
allUnits = sortUnits(allUnits)
logging.critical("\n==========COMBAT SIMULATION ENDED==========")
# Extra calculations
for char in playerTeam:
if char.name == "Yunli":
char.hitMultiplier = char.ults / totalEnemyAttacks
logging.critical("\n==========SIMULATION RESULTS==========")
# Print damage info
debuffDMG, charDMG = 0, 0
dmgList = []
for enemy in eTeam:
debuffDMG += enemy.debuffDMG
for char in playerTeam:
res, currCharDMG = char.getTotalDMG()
dmgList.append(currCharDMG)
charDMG += currCharDMG
dpavList = [i / avLimit for i in dmgList]
percentList = [i / dmg.getTotalDMG() * 100 for i in dmgList]
logging.critical(f"TOTAL TEAM DMG: {dmg.getTotalDMG():.3f} | AV: {avLimit}")
logging.critical(f"TEAM DPAV: {dmg.getTotalDMG() / avLimit:.3f}")
logging.critical(f"DEBUFF DMG: {dmg.getDebuffDMG():.3f} | CHAR DMG: {dmg.getActionDMG():.3f} | WB DMG: {dmg.getWeaknessBreakDMG():.3f}")
logging.critical(f"SP GAINED: {spTracker.getSPGain()} | SP USED: {spTracker.getSPUsed()} | Enemy Attacks: {totalEnemyAttacks}")
res = ""
i = 0
for char in playerTeam:
res += f"\n{char.name} DPAV: {dpavList[i]:.3f}, {percentList[i]:.3f}%"
i += 1
logging.critical(f"{res}\n")
for char in playerTeam:
res, charDMG = char.getTotalDMG()
logging.critical(f"{char.name} > Total DMG: {charDMG:.3f} | Basics: {char.basics} | Skills: {char.skills} | Ults: {char.ults} | FuAs: {char.fuas} | Leftover AV: {char.currAV:.3f} | Excess Energy: {char.currEnergy:.3f}")
logging.critical(res)
return f"DPAV: {dmg.getTotalDMG() / avLimit:.3f} | SP Used: {spTracker.getSPUsed()}, SP Gain: {spTracker.getSPGain()}"
if __name__ == "__main__":
# Start the simulator with logging output to a file
fiveEnemies = EnemyModule(5, [85, 85, 85, 85, 85], [EnemyType.ADD, EnemyType.ELITE, EnemyType.BOSS, EnemyType.ADD, EnemyType.ADD], [100, 120, 144, 100, 100], [20, 60, 70, 20, 20], atkRatio, [Element.LIGHTNING], [1])
print(startSimulator(cycleLimit=cycles, outputLog=log, manualMode=manual, enemyModule=fiveEnemies))