summaryrefslogblamecommitdiff
path: root/battle/common.py
blob: dadc49d8da41af5b35872f328bb8b5a1a325e1d7 (plain) (tree)
1
2
3
4
5
6
7
8
9



                                                                                        



                                                                          
 
                                                                     
                                                                    

                                                                   
 

                                                                           

                                                                                        
                                                                        







                                                                                 
             


                                             






                                    
                                             






                                    
                                             











































                                                                                 
                                             

                                              
                                                






                                                             

                                                                           

                
                                             













                                                           
                                                                                          
                   
                                                                                               




                                      
                                                   





                                                                                  
                                   

              




























































































































































































































































                                                                                                                                
 





                                                 
########################################################################################
#     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