########################################################################################
# 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 <http://www.gnu.org/licenses/>.
########################################################################################
# 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)
import random
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
i["hp"]-=calc_dmg(token, atker, i, atker["atk"], 0.0)
# If passed with a negative value...
if (i["hp"] > i["max_hp"]):
i["hp"]=0+i["max_hp"]
#Battle[token]["log"].append(["party", idx, sphere, 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"],
"status_effects": 0
})
# Update wave
stdout("Advancing wave", 2)
Battle[token]["wave"]+=1
return True
#############################################
# 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
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