######################################################################################## # This file is part of Spheres. # Copyright (C) 2019 Jesusalva # 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 . ######################################################################################## # Battle Module - Common functions from utils import stdout, Battle, compress, dl_search, allquests, Player from consts import (SC_ATKUP, SC_DEFDOWN, SC_DEFUP, SC_ATKDOWN, Ele_Fire, Ele_Water, Ele_Nature, Ele_Light, Ele_Shadow, ST_TOWN, CRYSTAL_MIN, CRYSTAL_MAX, EXPRATE_MIN, EXPRATE_MAX, GPPRATE_MIN, GPPRATE_MAX, SFLAG_CLEARGEMS, SFLAG_DOUBLEGEMS, SFLAG_SPECIAL, SFLAG_FIRSTLOOT, SFLAG_DOUBLEEXP, SFLAG_DOUBLEGP, SPH_WIDEATTACK, SPH_PIERCE, SPH_ASSAULT, SPH_HEAL, SPH_HEALALL, SPH_ATKUP, SPH_DEFUP, SPH_NONE, SRV_SPHERE, SRV_WAVE, SRV_NOCAST) import random, traceback import player ############################################# # Check victory condition def check_enemy_alive(token): for i in Battle[token]["enemy"]: if (i["hp"] > 0): return True return False ############################################# # Check defeat condition def check_player_alive(token): for i in Battle[token]["party"]: if (i["hp"] > 0): return True return False ############################################# # Calculate damage. Unit is a member from Battle[token][scope]. Base is dmg value # crit is a customizable critical chance def calc_dmg(token, attacker, defender, base, crit=0.1): sca=attacker["status_effects"] scd=defender["status_effects"] dmg=0+base # ATK/DEF up will double or not if (sca & SC_ATKUP): dmg*=2 if (scd & SC_DEFDOWN): dmg*=2 if (scd & SC_DEFUP): dmg/=2 if (sca & SC_ATKDOWN): dmg/=2 # Critical chance (Crit: +50% DMG) if (random.random() < crit): dmg*=1.5 # Elemental advantage: 30% ea=attacker["ele"] ed=defender["ele"] if (ea == Ele_Fire and ed == Ele_Water): dmg*=0.7 elif (ea == Ele_Fire and ed == Ele_Nature): dmg*=1.3 elif (ea == Ele_Water and ed == Ele_Nature): dmg*=0.7 elif (ea == Ele_Water and ed == Ele_Fire): dmg*=1.3 elif (ea == Ele_Nature and ed == Ele_Water): dmg*=0.7 elif (ea == Ele_Nature and ed == Ele_Fire): dmg*=1.3 elif (ea == Ele_Light and ed == Ele_Shadow): dmg*=1.3 elif (ea == Ele_Shadow and ed == Ele_Light): dmg*=1.3 # Impossible to move or miss is handled before return int(dmg) ############################################# # Attack all, scope can be: "enemy" or "party" def attackall(token, atker, scope): for c, i in enumerate(Battle[token][scope]): if (i["hp"] < 0): continue dmg = calc_dmg(token, atker, i, atker["atk"], 0.0) i["hp"]-=dmg # If passed with a negative value... if (i["hp"] > i["max_hp"]): i["hp"]=0+i["max_hp"] Battle[token]["log"].append(["", 0, SRV_NOCAST, dmg, scope, c]) return False ############################################# # Find and return target ID in Battle[token][scope]. def find_target(token, scope): global Battle targets=[] i=-1 #stdout("Request to find a target on scope %s" % scope) #stdout("Length is: %d" % len(Battle[token][scope])) while (i+1) < len(Battle[token][scope]): i+=1 try: if (Battle[token][scope][i]["hp"] < 0): continue except: try: stdout("(%d) Faulty structure: %s" % (i, str(Battle[token][scope][i])), 0) except: stdout("(%d) TOTALLY Faulty structure: %s" % (i, str(Battle[token][scope])), 0) continue continue targets.append(i) #stdout("While loop has finished") stdout("List of targets: %s" % str(targets), 2) # When a player selects an target, it should always be the first enemy in list if scope == "enemy": opt=targets[0] else: opt=random.choice(targets) stdout("Selected: %d" % opt, 2) return opt ############################################# # Structural functions ############################################# def conditions(token, spheres): global Battle, Player ## Maybe you won? if (not check_enemy_alive(token)): # You won! Do we have a next wave? stdout("You won!") if (Battle[token]["wave"] < Battle[token]["max_wave"]): stdout("Next wave detected: %s, Quest ID: %s\nWave: %s" % (token, Battle[token]["quest_id"], Battle[token]["wave"])) advance_wave(token, Battle[token]["world"], Battle[token]["quest_id"], Battle[token]["wave"]) # Do not continue this loop: End the turn now return battle_endturn(token, spheres) else: # You won! stdout("Total Victory") Player[token]["status"]=ST_TOWN # TODO: enemy die = add reward result=get_result(token, True, Battle[token]["world"], Battle[token]["quest_id"]) del Battle[token] return compress(result) ## But maybe you lost? if (not check_player_alive(token)): # You lost! Player[token]["status"]=ST_TOWN result=get_result(token, False, Battle[token]["world"], Battle[token]["quest_id"]) del Battle[token] return compress(result) ## Return None so we know nothing happened return None ############################################# # Update enemy list with the next wave data. No checks are conducted. def advance_wave(token, world, quest_id, next_wave): global Battle, allquests Battle[token]["enemy"]=[] Battle[token]["log"].append(["", 0, SRV_WAVE, 0, "", 0]) stdout("advance_wave was called") quest=dl_search(allquests[world], "quest_id", quest_id) if quest == "ERROR": stdout("ERROR, INVALID QUEST: %d" % (quest_id), 0) # TODO: HANDLE THIS ERROR (FIXME) for en in quest["waves"][next_wave]: mil=quest["difficulty"]/10.0 if (en["boss"]): mil*=5.5 mil=mil stdout("Recording new enemy with mil: %d" % mil, 2) Battle[token]["enemy"].append({ "name": en["name"], "unit_id": en["sprite"], "max_hp": int(900*mil), "hp": int(900*mil), "atk": int(100*mil), "ele": en["attribute"], "job": 2, # FIXME "status_effects": 0 }) # Update wave stdout("Advancing wave", 2) Battle[token]["wave"]+=1 return True ################################################## # Levels up your summoning skill qids = [-1, 7] def summonlv(token, qid): try: level = qids.index(qid) stdout("Adjusting player summon level (%d/%d)" % (Player[token]["max_sum"], level), 2) Player[token]["max_sum"] = max(Player[token]["max_sum"], level) except ValueError: stdout("Quest does not affect summoning", 2) except: traceback.print_exc() stdout("Error at summoning level check for quest %d" % qid) return ############################################# # get_result(str, bool, str, int) def get_result(token, victory, world, quest_id): global Player, Battle, allquests result={ "result": "", "gp": 0, "exp": 0, "crystals": 0, "loot": [], "rank": 0, "log": Battle[token]["log"] } stdout("GR: Begin", 2) # You lost? if not victory: result["result"]="DEFEAT" return result # Prepare data result["result"]="VICTORY" quest=dl_search(allquests[world], "quest_id", quest_id) if quest == "ERROR": print("ERROR, INVALID QUEST") stdout("Quest %d is invalid" % quest_id, 0) # TODO: HANDLE THIS ERROR (FIXME) return result # Check if your summoning level has improved summonlv(token, quest_id) stdout("GR: Rolling", 2) # Roll each wave # Base quest experience gain result["exp"]+=quest["difficulty"]*10 for wave in quest["waves"]: for en in wave: # Roll GP for each wave if en["boss"]: result["gp"]+=quest["difficulty"]*30 result["exp"]+=quest["difficulty"] else: result["gp"]+=quest["difficulty"]*10 result["exp"]+=quest["difficulty"]/3 # TODO: Roll the loots for every enemy in every death result["exp"]=int(result["exp"]) stdout("GR: Looting", 2) # For now, loots are rolled for quest # 2- Roll loot list for loot, chance in quest["loot"]: if (random.randint(0, 10000) < chance): loot=int(loot) # Crystals are... Tricky if loot >= CRYSTAL_MIN and loot <= CRYSTAL_MAX: result["crystals"]+=(loot-CRYSTAL_MIN) elif loot >= EXPRATE_MIN and loot <= EXPRATE_MAX: result["exp"]*=(loot-EXPRATE_MIN)/1000.0 result["exp"]=int(result["exp"]) elif loot >= GPPRATE_MIN and loot <= GPPRATE_MAX: result["gp"]*=(loot-GPPRATE_MIN)/1000.0 result["gp"]=int(result["gp"]) else: result["loot"].append(int(loot)*100) # Fix Unit ID from base to ID stdout("GR: Flagging", 2) # Mark the quest as complete and grant you crystals for first clear # But this is based on the flags (we can have special quests) if (not (quest["flags"] & SFLAG_SPECIAL) and Player[token]["quest"] < quest["quest_id"]): Player[token]["quest"]=0+quest["quest_id"] if (quest["flags"] & SFLAG_CLEARGEMS): result["crystals"]+=100 if (quest["flags"] & SFLAG_DOUBLEGEMS): result["crystals"]+=200 if (quest["flags"] & SFLAG_FIRSTLOOT): loot=int(quest["loot"][0][0]) # Crystals are... Tricky if loot >= CRYSTAL_MIN and loot <= CRYSTAL_MAX: result["crystals"]+=(loot-CRYSTAL_MIN) else: result["loot"].append(int(loot)*100) # Fix Unit ID from base to ID if (quest["flags"] & SFLAG_DOUBLEEXP): result["exp"]*=2 if (quest["flags"] & SFLAG_DOUBLEGP): result["gp"]*=2 stdout("GR: Applying", 2) # Apply the results to player data Player[token]["gp"]+=result["gp"] Player[token]["exp"]+=result["exp"] Player[token]["crystals"]+=result["crystals"] for it in result["loot"]: ix=player.inventoryplace(token) if (ix >= 0): unit={ "unit_id": it, "level": 1, "exp": 0 } Player[token]["inv"][ix]=unit else: result["loot"].remove(it) # TODO: Send result ERR_FULL stdout("GR: EXPing", 2) # Grant to party the same amount of experience pid=Battle[token]["party_id"] for m in Player[token]["party_"+str(pid)]: # This can happen normally if m["unit_id"] <= 0: continue player.grant_exp(token, m["inv_id"], result["exp"]) v5=player.check_level_up(token, m["inv_id"]) # FIXME: Don't disregard result stdout("Unit %d levelled up %d times" % (m["inv_id"], v5)) stdout("GR: Ranking", 2) # Player rank up rk=player.check_rank_up(token) if rk["code"]: result["rank"]=1+rk["ap"] result["rk_exp"]=Player[token]["exp"] result["rk_mexp"]=Player[token]["max_exp"] stdout("GR: Complete") # Send data return result ############################################# # This is separate for loop logic reasons. Ends the turn. # Gives sphere, handle rewards if needed and sends data to client def battle_endturn(token, spheres): global Battle # We now have to handle turn end and conditions Battle[token]["turn"]+=1 stdout("Turn has ended") # Resave spheres stdout("Sphere list: "+str(spheres), 2) Battle[token]["spheres"]=[] for i in spheres: Battle[token]["spheres"].append(int(i)) # Remove an eventual none spheres while (Battle[token]["spheres"].count(SPH_NONE)): Battle[token]["spheres"].remove(SPH_NONE) stdout("Exceeding spheres removed", 2) # Add 1~3 random spheres; But never go over 5 spheres i=min(random.randint(1,3), 5 - len(Battle[token]["spheres"])) while i > 0: i-=1 sp=random.choice([ SPH_WIDEATTACK, SPH_PIERCE, SPH_ASSAULT, SPH_HEAL, SPH_HEALALL, SPH_ATKUP, SPH_DEFUP ]) Battle[token]["log"].append(["spheres", len(Battle[token]["spheres"]), SRV_SPHERE, sp, "", 0]) Battle[token]["spheres"].append(sp) stdout("Spheres added; Status before adjust: %s" % (str(Battle[token]["spheres"])), 2) # Add empty spheres to keep length while (len(Battle[token]["spheres"]) < 5): Battle[token]["spheres"].append(SPH_NONE) # Delete Summon data if exists try: del Battle["s"] except: pass # Send data to client stdout("Sending data", 2) sjson=compress(Battle[token]) stdout("Data sent: %s" % sjson) return sjson ################################################# # Handles CI false positives in a lame way def pyflakes(): return allquests, Player