summaryrefslogtreecommitdiff
path: root/battle
diff options
context:
space:
mode:
authorJesusaves <cpntb1@ymail.com>2020-12-18 18:52:58 -0300
committerJesusaves <cpntb1@ymail.com>2020-12-18 18:52:58 -0300
commit228e91fe7e4f671dd593b5e288e812e287a65a00 (patch)
treec5e0f055c80a62d4abfbe80e24c8bbdbfaef1701 /battle
parent194af986ceeb72a2c0173e8e414b9e1a106495bf (diff)
downloadserver-228e91fe7e4f671dd593b5e288e812e287a65a00.tar.gz
server-228e91fe7e4f671dd593b5e288e812e287a65a00.tar.bz2
server-228e91fe7e4f671dd593b5e288e812e287a65a00.tar.xz
server-228e91fe7e4f671dd593b5e288e812e287a65a00.zip
Initial battle version imported from Python2 and with pyflakes3 applied.
Divided in multiple files for ease. UNTESTED. (But server still runs)
Diffstat (limited to 'battle')
-rw-r--r--battle/__init__.py21
-rw-r--r--battle/common.py126
-rw-r--r--battle/main.py540
-rw-r--r--battle/skills.py86
-rw-r--r--battle/spheres.py79
-rw-r--r--battle/summons.py110
6 files changed, 962 insertions, 0 deletions
diff --git a/battle/__init__.py b/battle/__init__.py
new file mode 100644
index 0000000..90240ea
--- /dev/null
+++ b/battle/__init__.py
@@ -0,0 +1,21 @@
+########################################################################################
+# This file is part of Spheres.
+# Copyright (C) 2019 Jesusalva
+
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+
+# This library 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
+# Lesser General Public License for more details.
+
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+########################################################################################
+# No idea what I'm doing
+
+
diff --git a/battle/common.py b/battle/common.py
new file mode 100644
index 0000000..ec0d9d7
--- /dev/null
+++ b/battle/common.py
@@ -0,0 +1,126 @@
+########################################################################################
+# This file is part of Spheres.
+# Copyright (C) 2019 Jesusalva
+
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+
+# This library 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
+# Lesser General Public License for more details.
+
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+########################################################################################
+# Battle Module - Common functions
+from utils import stdout
+from consts import (SC_ATKUP, SC_DEFDOWN, SC_DEFUP, SC_ATKDOWN, Ele_Fire,
+ Ele_Water, Ele_Nature, Ele_Light, Ele_Shadow)
+import random
+# 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 i in 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"]
+ 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:
+ print("(%d) Faulty structure: %s" % (i, str(Battle[token][scope][i])))
+ except:
+ print("(%d) TOTALLY Faulty structure: %s" % (i, str(Battle[token][scope])))
+ continue
+ continue
+ targets.append(i)
+
+ #stdout("While loop has finished")
+ stdout("List of targets: %s" % str(targets))
+
+ # 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)
+ return opt
+
+
diff --git a/battle/main.py b/battle/main.py
new file mode 100644
index 0000000..10b8cdf
--- /dev/null
+++ b/battle/main.py
@@ -0,0 +1,540 @@
+########################################################################################
+# This file is part of Spheres.
+# Copyright (C) 2019 Jesusalva
+
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+
+# This library 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
+# Lesser General Public License for more details.
+
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+########################################################################################
+# Battle Module - Main module
+from utils import stdout, dl_search, compress, allunits
+from consts import (CRYSTAL_MIN, CRYSTAL_MAX, EXPRATE_MIN, EXPRATE_MAX,
+ GPPRATE_MIN, GPPRATE_MAX, ERR_ERR, ERR_BAD, ST_TOWN, ST_QUEST, ACT_SPHERE,
+ 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)
+import json, random
+import player
+from battle.common import (find_target, check_enemy_alive, check_player_alive,
+ calc_dmg)
+from battle.spheres import sphere_attack
+#from battle.summons import summon # <-- Protocol?
+#from battle.skills import skill_core, handle_skill # <-- WIP
+
+# Prepare a randomness seed
+random.seed()
+
+#############################################
+# Structural functions
+#############################################
+# Update enemy list with the next wave data. No checks are conducted.
+def advance_wave(token, quest_id, next_wave):
+ global Battle, allquests
+ Battle[token]["enemy"]=[]
+ stdout("advance_wave was called")
+
+ quest=dl_search(allquests, "quest_id", quest_id)
+ if quest == "ERROR":
+ print("ERROR, INVALID QUEST")
+ # 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)
+ Battle[token]["enemy"].append({
+ "name": en["name"],
+ "unit_id": en["sprite"],
+ "max_hp": int(1000*mil),
+ "hp": int(1000*mil),
+ "atk": int(100*mil),
+ "ele": en["attribute"],
+ "status_effects": 0
+ })
+
+ # Update wave
+ stdout("Advancing wave")
+ Battle[token]["wave"]+=1
+ return True
+
+# get_result(str, bool, int)
+def get_result(token, victory, quest_id):
+ global Player, Battle, allquests
+ result={
+ "result": "",
+ "gp": 0,
+ "exp": 0,
+ "crystals": 0,
+ "loot": [],
+ "rank": 0
+ }
+
+ stdout("GR: Begin")
+ # You lost?
+ if not victory:
+ result["result"]="DEFEAT"
+ return result
+
+ # Prepare data
+ result["result"]="VICTORY"
+ quest=dl_search(allquests, "quest_id", quest_id)
+ if quest == "ERROR":
+ print("ERROR, INVALID QUEST")
+ stdout("Quest %d is invalid", str(quest_id))
+ # TODO: HANDLE THIS ERROR (FIXME)
+ return result
+
+ stdout("GR: Rolling")
+ # Roll each wave
+ 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"]*50
+ else:
+ result["gp"]+=quest["difficulty"]*10
+ result["exp"]+=quest["difficulty"]*10
+
+ # TODO: Roll the loots for every enemy in every death
+
+ stdout("GR: Looting")
+ # 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")
+ # 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")
+ # 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")
+ # 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")
+ # Player rank up
+ rk=player.check_rank_up(token)
+ if rk["code"]:
+ result["rank"]=1+rk["ap"]
+
+ stdout("GR: Complete")
+
+ # Send data
+ return result
+
+#############################################
+# Client commands
+#############################################
+# begin_quest( quest_id, party_id )
+def begin_quest(args, token, client_side=True):
+ global allquests, Player, Battle
+ # Data validation
+ try:
+ stdout("Begin Quest: %s" % args)
+ bq=json.loads(args)
+ stdout("JSON loaded")
+ # Validation
+ tmp=int(bq["quest_id"])
+ tmp=int(bq["party_id"])
+ if (tmp < 1 or tmp > 3):
+ return ERR_BAD
+ tmp=int(Player[token]["quest"])
+ for quest in allquests:
+ if (quest["quest_id"] == bq["quest_id"]):
+ break
+ if (quest["quest_id"] != bq["quest_id"]):
+ raise Exception("Quest not found")
+ stdout("All fine thus far")
+ except:
+ # Invalid data
+ return ERR_BAD
+
+ # We have `quest` variable loaded and now we'll only use it, but...
+
+ # Can you start a quest? (In Town, Requeriments met, etc.)
+ if (Player[token]["status"] != ST_TOWN and client_side):
+ return ERR_BAD
+ if (Player[token]["quest"] < quest["requeriment"]):
+ return ERR_BAD
+ if (Player[token]["ap"] < quest["cost"] and client_side):
+ return ERR_BAD
+ stdout("You can begin it!")
+
+
+ Player[token]["status"]=int(quest["quest_id"])
+ # client_side is an internal flag
+ if client_side:
+ #Player[token]["ap"]-=quest["cost"]
+ player.update_ap(token, -(quest["cost"]))
+
+ Battle[token]={
+ "quest_id": quest["quest_id"],
+ "difficulty": quest["difficulty"],
+ "result": "",
+ "bp": 0,
+ "wave": 0,
+ "max_wave": len(quest["waves"]),
+ "enemy": [],
+ "party": [],
+ "party_id": int(bq["party_id"]),
+ "spheres": [],
+ "turn": 1}
+
+ stdout("Data set")
+ # Write the current wave enemies data
+ advance_wave(token, quest["quest_id"], 0)
+
+ # Write the current party (and their HP bar)
+ for ux in Player[token]["party_"+str(int(bq["party_id"]))]:
+ # If you don't have anyone here: Don't append to party
+ if ux["unit_id"] == 0:
+ continue
+
+ #stdout("FOUND AN UX: %s" % str(ux))
+ # Un: Entry in unit database. Ux: Entry on party.
+ # TODO: Maybe use dl_search instead
+ for un in allunits:
+ if (un["unit_id"] == ux["unit_id"]):
+ #stdout("[BEGIN_QUEST] Match found for un and ux unit_id (un is: %s)" % str(un))
+ break
+ else:
+ continue
+
+ # Wait, what?
+ if (un["unit_id"] != ux["unit_id"]):
+ print("Error, un and ux differs: %s vs %s" % (str(un["unit_id"]), str(ux["unit_id"])))
+ return ERR_ERR
+
+ stdout("Preparing party member")
+
+ # How much HP/ATK bonus per level you'll get?
+ lvl=int(Player[token]["inv"][ux["inv_id"]]["level"])-1
+ hpboost=player.readjust_status(un["rare"], un["job"], True)*max(lvl-1, 0)
+ akboost=player.readjust_status(un["rare"], un["job"], False)*max(lvl-1, 0)
+
+ # generate unit data for battle
+ Battle[token]["party"].append({
+ "unit_id": un["unit_id"],
+ "max_hp": un["hp"]+hpboost,
+ "hp": un["hp"]+hpboost,
+ "atk": un["strength"]+akboost,
+ "ele": un["attribute"],
+ "status_effects": 0
+ })
+
+ # Add 1~3 random spheres
+ i=random.randint(1,3)
+ while i > 0:
+ i-=1
+ Battle[token]["spheres"].append(random.choice([
+ SPH_WIDEATTACK,
+ SPH_PIERCE,
+ SPH_ASSAULT,
+ SPH_HEAL,
+ SPH_HEALALL,
+ SPH_ATKUP,
+ SPH_DEFUP
+ ]))
+
+ # Add empty spheres to keep length
+ while (len(Battle[token]["spheres"]) < 5):
+ Battle[token]["spheres"].append(SPH_NONE)
+
+ sjson=compress(Battle[token])
+ return sjson
+
+
+
+# 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))
+ 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")
+
+ # Add 1~3 random spheres
+ i=random.randint(1,3)
+ while i > 0:
+ i-=1
+ Battle[token]["spheres"].append(random.choice([
+ SPH_WIDEATTACK,
+ SPH_PIERCE,
+ SPH_ASSAULT,
+ SPH_HEAL,
+ SPH_HEALALL,
+ SPH_ATKUP,
+ SPH_DEFUP
+ ]))
+
+ stdout("Spheres added; Status before adjust: %s" % (str(Battle[token]["spheres"])))
+
+ # Remove exceeding spheres
+ if (len(Battle[token]["spheres"]) > 5):
+ del(Battle[token]["spheres"][4:])
+
+ # 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")
+ sjson=compress(Battle[token])
+ stdout("Data sent: %s" % sjson)
+ return sjson
+
+# Advance turn button. Main turn logic
+# Receives a list of units position, and if they'll use sphere:
+# { "unit": [uid1, uid2 ....], "sphere": [True/False array, in future will save skill as 2] }
+def battle(args, token):
+ global allquests, Player, Battle
+ # Data validation
+ try:
+ stdout("Advance round: %s" % args)
+ bt=json.loads(args)
+ stdout("JSON loaded")
+ # Validation
+ tmp=len(bt["unit"])
+ stdout("vu: %d" % tmp)
+ tmp=len(bt["sphere"])
+ stdout("vs: %d" % tmp)
+ tmp=int(Player[token]["quest"])
+ stdout("ptq: %d pts: %d" % (tmp, Player[token]["status"]))
+ if (Player[token]["status"] < ST_QUEST):
+ return ERR_BAD
+ stdout("validation step 1")
+
+ # Check if all members you've told us are valid
+ for j in bt["unit"]:
+ i=False
+ for k in Battle[token]["party"]:
+ if (k["unit_id"] == j["unit_id"]):
+ i=True
+ break
+ if not i:
+ stdout("Supplied data is valid: Attempt to add invalid members")
+ raise Exception("Attempted to add invalid members")
+ except:
+ # Invalid data
+ return ERR_BAD
+
+ #######################################
+ # Reorder your party based on your request. We already know its valid
+
+ tmp=[]
+ for bat in Battle[token]["party"]:
+ tmp.append(bat)
+
+ Battle[token]["party"]=[]
+
+ for j in bt["unit"]:
+ for k in tmp:
+ if (j["unit_id"] == k["unit_id"]):
+ Battle[token]["party"].append(k)
+ break
+
+ # Erase temporary variables
+ tmp=[]
+ stdout("Party reordered")
+
+ #######################################
+ # Prepare temporary data
+ spheres=[]
+ for i in Battle[token]["spheres"]:
+ spheres.append(int(i))
+ print("Sphere list: "+str(spheres))
+
+ #######################################
+ # TODO: determine battle order
+
+ ############
+ # Friends
+ for bat in Battle[token]["party"]:
+ # The dead can't fight
+ if (bat["hp"] <= 0):
+ continue
+
+ # Will you use a sphere?
+ idx=Battle[token]["party"].index(bat)
+ stdout("Check index %d" % idx)
+ if (bt["sphere"][idx] == ACT_SPHERE):
+ # Remove the sphere
+ spheres[idx]=SPH_NONE
+ sphere_attack(token, bat, Battle[token]["spheres"][idx])
+ Battle[token]["bp"]+=2
+ else:
+ sphere_attack(token, bat, SPH_NONE)
+ Battle[token]["bp"]+=1
+ stdout("Attack performed")
+
+ # If HP ends up broken
+ if (bat["hp"] > bat["max_hp"]):
+ bat["hp"]=0+bat["max_hp"]
+
+ # HOLD THAT! Handle event
+ 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]["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]["quest_id"])
+ del Battle[token]
+ return compress(result)
+
+ stdout("Friends executed")
+
+ # It never harms checking for suicide...
+ if (not check_player_alive(token)):
+ # You lost!
+ Player[token]["status"]=ST_TOWN
+ result=get_result(token, False, Battle[token]["quest_id"])
+ del Battle[token]
+ return compress(result)
+
+ ############
+ # Enemies
+ for bat in Battle[token]["enemy"]:
+ # The dead can't fight
+ if (bat["hp"] <= 0):
+ continue
+
+ # Simple Attack
+ target_id=find_target(token, "party")
+ stdout("ENEMY: Perform attack against %d" % target_id)
+ target=Battle[token]["party"][target_id]
+ target["hp"]-=calc_dmg(token, bat, target, bat["atk"])
+ Battle[token]["bp"]+=1
+
+ stdout("ENEMY: Attack performed")
+
+ # If HP ends up broken
+ if (bat["hp"] > bat["max_hp"]):
+ bat["hp"]=0+bat["max_hp"]
+ continue
+
+ # HOLD THAT! Handle event
+ if (not check_player_alive(token)):
+ # You lost!
+ Player[token]["status"]=ST_TOWN
+ result=get_result(token, False, Battle[token]["quest_id"])
+ del Battle[token]
+ return compress(result)
+
+
+ stdout("Enemies executed")
+ return battle_endturn(token, spheres)
+
+# Reload battle will only inform the client again of the battle status
+# If in a battle, otherwise it returns: ERR_BAD
+# TODO FIXME: Do not default to PID 1. Probably save it as a mask.
+# eg. PID + QID as string. So ST_QUEST becomes 11 (PID 1 QID 1)
+def reload_battle(token):
+ if (Player[token]["status"] < ST_QUEST):
+ return ERR_BAD
+
+ try:
+ bq=0+int(Battle[token]["quest_id"])
+ del bq
+ return compress(Battle[token])
+ except:
+ v1=begin_quest('{"quest_id": %d, "party_id": 1}' % Player[token]["status"], token, client_side=False)
+ stdout("Restarting quest...")
+ return v1
+
+
diff --git a/battle/skills.py b/battle/skills.py
new file mode 100644
index 0000000..180ec9c
--- /dev/null
+++ b/battle/skills.py
@@ -0,0 +1,86 @@
+########################################################################################
+# This file is part of Spheres.
+# Copyright (C) 2019 Jesusalva
+
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+
+# This library 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
+# Lesser General Public License for more details.
+
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+########################################################################################
+# Battle Module - Skills
+from utils import dbgprint
+from consts import (SK_SINGLE_DMG, SK_MULTI_DMG, SK_ATK_UP, SK_DEF_UP,
+ SK_SINGLE_HEAL, SK_MULTI_HEAL, SC_ATKUP, SC_DEFUP)
+from battle.common import find_target, calc_dmg, attackall
+def skill_core(token, mask, force, element):
+ global Battle
+
+ # Create a fake unit
+ dummy={
+ "name": "Dummy",
+ "unit_id": 1,
+ "max_hp": 1,
+ "hp": 1,
+ "atk": force,
+ "ele": element,
+ "status_effects": 0
+ }
+
+ # Handle the mask to determine the action
+ # TODO: SK_CLEAR_SC, SK_RESSURECTION
+ if mask & SK_SINGLE_DMG:
+ # Single Damage skill: 1 strength = 1 damage
+ target_id=find_target(token, "enemy")
+ dbgprint("Enemy selected: %d" % target_id)
+ target=Battle[token]["enemy"][target_id]
+ target["hp"]-=calc_dmg(token, dummy, target, dummy["atk"])
+
+ if mask & SK_MULTI_DMG:
+ # Multi Damage skill: 1 strength = 1 damage
+ attackall(token, dummy, "enemy")
+
+ if mask & SK_ATK_UP:
+ # Attack up skill: Add SC_ATKUP to all members (even dead ones)
+ for target in Battle[token]["party"]:
+ if not (target["status_effects"] & SC_ATKUP):
+ target["status_effects"]=target["status_effects"]|SC_ATKUP
+
+ if mask & SK_DEF_UP:
+ # Defense up skill: Add SC_DEFUP to all members (even dead ones)
+ for target in Battle[token]["party"]:
+ if not (target["status_effects"] & SC_DEFUP):
+ target["status_effects"]=target["status_effects"]|SC_DEFUP
+
+ # TODO: Is single heal the same as self heal?
+ if mask & SK_SINGLE_HEAL:
+ # Heal skill: 1 strength = 1% healing (FIXME?)
+ target_id=find_target(token, "party")
+ target=Battle[token]["party"][target_id]
+ target["hp"]+=int(target["max_hp"]*dummy["atk"]/100)
+ if (target["hp"] > target["max_hp"]):
+ target["hp"]=0+target["max_hp"]
+
+ if mask & SK_MULTI_HEAL:
+ # Heal skill: 1 strength = 1 HP (FIXME?)
+ dummy["atk"]*=-1
+ attackall(token, dummy, "party")
+
+ # Victory/Defeat checks are not handled here
+ # This doesn't returns anything, actually.
+ # Errors must be handled as Exceptions
+ return
+
+def handle_skill(token, skill_id):
+ global Battle
+ # TODO
+ return
+
diff --git a/battle/spheres.py b/battle/spheres.py
new file mode 100644
index 0000000..aa2e756
--- /dev/null
+++ b/battle/spheres.py
@@ -0,0 +1,79 @@
+########################################################################################
+# This file is part of Spheres.
+# Copyright (C) 2019 Jesusalva
+
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+
+# This library 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
+# Lesser General Public License for more details.
+
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+########################################################################################
+# Battle Module - Spheres
+from utils import stdout, Battle
+from consts import (SPH_WIDEATTACK, SPH_PIERCE, SPH_ASSAULT, SPH_HEAL, SPH_HEALALL,
+ SPH_NONE, SPH_ATKUP, SPH_DEFUP, SC_ATKUP, SC_DEFUP)
+from battle.common import find_target, attackall, calc_dmg
+
+# Handle sphere attacks. Unit is a member from Battle[token][scope]
+def sphere_attack(token, unit, sphere):
+ stdout("%s is attacking" % str(unit))
+ stdout("Sphere selected: %d" % sphere)
+
+ # Select the enemy which should be attacked
+ target_id=find_target(token, "enemy")
+ stdout("Enemy selected: %d" % target_id)
+ target=Battle[token]["enemy"][target_id]
+
+ # Now cycle though spheres
+ if (sphere == SPH_WIDEATTACK):
+ # Wide Attack sphere
+ attackall(token, unit, "enemy")
+ elif (sphere == SPH_PIERCE):
+ # Pierce sphere (100% critical chance)
+ target["hp"]-=calc_dmg(token, unit, target, unit["atk"], 1.0)
+ elif (sphere == SPH_ASSAULT):
+ # Assault sphere (50% critical chance)
+ target["hp"]-=calc_dmg(token, unit, target, unit["atk"], 0.5)
+ elif (sphere == SPH_HEAL):
+ # Heal sphere (30% healing). Need to redefine target
+ target_id=find_target(token, "party")
+ target=Battle[token]["party"][target_id]
+ target["hp"]+=int(target["max_hp"]*0.3)
+ if (target["hp"] > target["max_hp"]):
+ target["hp"]=0+target["max_hp"]
+ elif (sphere == SPH_HEALALL):
+ # Heal All (negative damage to party)
+ unit["atk"]*=-1
+ attackall(token, unit, "party")
+ unit["atk"]*=-1
+ elif (sphere == SPH_NONE):
+ # Normal Attack
+ stdout("It's a normal attack!")
+ target["hp"]-=calc_dmg(token, unit, target, unit["atk"])
+ elif (sphere == SPH_ATKUP):
+ # Attack up sphere: Add SC_ATKUP to all members (even dead ones)
+ for target in Battle[token]["party"]:
+ if not (target["status_effects"] & SC_ATKUP):
+ target["status_effects"]=target["status_effects"]|SC_ATKUP
+ elif (sphere == SPH_DEFUP):
+ # Defense up sphere: Add SC_DEFUP to all members (even dead ones)
+ for target in Battle[token]["party"]:
+ if not (target["status_effects"] & SC_DEFUP):
+ target["status_effects"]=target["status_effects"]|SC_DEFUP
+ else:
+ # Something went wrong, report and do nothing
+ print ("\033[1;31mERROR, INVALID SPHERE ID: %d\033[0m\n" % sphere)
+ return False
+
+ # We are done!
+ #stdout("Attack completed")
+ return True
+
diff --git a/battle/summons.py b/battle/summons.py
new file mode 100644
index 0000000..ec30868
--- /dev/null
+++ b/battle/summons.py
@@ -0,0 +1,110 @@
+########################################################################################
+# This file is part of Spheres.
+# Copyright (C) 2019 Jesusalva
+
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+
+# This library 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
+# Lesser General Public License for more details.
+
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+########################################################################################
+# Battle Module - Summons
+import json
+from utils import dbgprint, compress, allsummons, stdout, dl_search
+from consts import (ERR_ERR, ERR_BAD, ST_TOWN)
+from battle.skills import skill_core
+from battle.common import check_enemy_alive, check_player_alive
+from battle.main import advance_wave, battle_endturn, get_result
+
+def handle_summon(token, summon):
+ global Battle, Player
+
+ try:
+ # Summon strength is based on player rank
+ force=summon["strength"]*Player[token]["level"]
+
+ # Cast the skill, return error 500 on exception
+ skill_core(token, summon["type"], force, summon["attribute"])
+ except:
+ return ERR_ERR
+
+ # Check for victory/defeat conditions
+ # Huge code duplication
+
+ dbgprint("Summon: Victory/Defeat Check")
+ # HOLD THAT! Handle event
+ if (not check_enemy_alive(token)):
+ # You won! Do we have a next wave?
+ dbgprint("You won!")
+ if (Battle[token]["wave"] < Battle[token]["max_wave"]):
+ dbgprint("Next wave detected: %s, Quest ID: %s\nWave: %s" % (token, Battle[token]["quest_id"], Battle[token]["wave"]))
+ advance_wave(token, Battle[token]["quest_id"], Battle[token]["wave"])
+ # Do not continue this loop: End the turn now
+ return battle_endturn(token, Battle[token]["spheres"])
+ else:
+ # You won!
+ dbgprint("Total Victory")
+ Player[token]["status"]=ST_TOWN
+ # TODO: enemy die = add reward
+ result=get_result(token, True, Battle[token]["quest_id"])
+ del Battle[token]
+ return compress(result)
+
+ # It never harms checking for suicide...
+ if (not check_player_alive(token)):
+ # You lost!
+ Player[token]["status"]=ST_TOWN
+ result=get_result(token, False, Battle[token]["quest_id"])
+ del Battle[token]
+ return compress(result)
+
+ # The check is: if not ERR_ERR or ERR_BAD, then reload battle/result
+ sjson=compress(Battle[token])
+ return sjson
+
+
+def summon(args, token):
+ # Data validation
+ try:
+ stdout("Summon: %s" % args)
+ ss=json.loads(args)
+ # [summon_id]
+ # Validation
+ summon_id=int(ss[0])
+
+ # Create summon object
+ summon=dl_search(allsummons, "summon_id", summon_id)
+ if summon == "ERROR":
+ stdout("ERROR, INVALID SUMMON ID %d" % summon_id)
+ raise Exception("Invalid summon")
+
+ # Verify the cost
+ if Battle[token]["bp"] < summon["cost"]:
+ stdout("Cannot summon \"%s\": Insufficient BP (%d/%d)" % (summon["name"], Battle[token]["bp"], summon["cost"]))
+ raise Exception("Insufficient BP")
+
+ stdout("All fine thus far")
+ except:
+ # Invalid data
+ return ERR_BAD
+
+ # Already summoned (reverse logic)
+ try:
+ Battle["s"]+=1
+ Battle["s"]-=1
+ return ERR_BAD
+ except:
+ pass
+
+ Battle[token]["bp"]-=summon["cost"]
+ Battle["s"]=1
+ return handle_summon(token, summon)
+