HLPL / VerTeX Modelling Language Documentation

← examples

# MMORPG Skill Rotation Optimizer (Rogue Class)
# Optimizes skill rotation to maximize total damage output
# Subject to energy/combo point resource constraints and ability cooldowns
# Demonstrates temporal sequencing with time-indexed variables and resource tracking

model max_damage
set S = getSkills()                                         # Available skills
set T = getTime()                                           # Time steps in rotation
set F = {0}                                                 # Singleton set for constant indexing
set G = {0, 1}                                              # Energy level options

# Continuous variables for resource tracking
var 0 <= e <= 100, forall t in T                            # Energy at time t
var 0 <= k <= 5, forall t in T                              # Combo points at time t
var 0 <= pi <= 100, forall t in T                           # Energy spent at time t
var 0 <= gamma <= 5, forall t in T                          # Combo points spent at time t

# Skill properties loaded from CSV
const d = getDamage(), forall s in S                        # Base damage of skill s
const epsilon = getEnergy(), forall s in S                  # Energy cost/gain of skill s (negative = cost)
const delta = {100, 150}, forall g in G                     # Starting energy levels
const kappa = getComboPoints(), forall s in S               # Combo points generated/consumed by skill s
const tau = getCooldowns(), forall s in S                   # Cooldown duration of skill s
const rho = {0.2}, forall f in F                            # Energy regeneration rate
const r = {10}, forall f in F                               # Energy regeneration per tick
const M = {323}, forall f in F                              # Big-M constant for logical constraints
const psi = getFinishingMove(), forall s in S               # 1 if skill is finishing move, 0 otherwise

# Decision variables
bin x forall t in T, forall s in S                          # 1 if skill s is used at time t
bin y, forall t in T, forall s in S                         # Auxiliary variable for constraints

# Objective: Maximize total damage over the rotation
max sum_{s in S}{sum_{t in T}{x_{t,s} * d_{s}}}

# Energy balance constraint (for t > 0)
# Energy at time t = energy at t-1 + energy from skills - energy spent + regeneration
constr e_{t} = e_{t-1} + sum_{s in S}{epsilon_{s} * x_{t,s}} - pi_{t} + r_{0} * rho_{0},
  forall t in T setminus {0}

# Combo point balance constraint (for t > 0)
# Combo points at time t = combo points at t-1 + points generated - points consumed
constr k_{t} = k_{t-1} + sum_{s in S}{kappa_{s} * x_{t,s}} - gamma_{t},
  forall t in T setminus {0}

# Exactly one skill must be used at each time step
constr sum_{s in S}{x_{t,s}} = 1, forall t in T

# Finishing move constraint: If finishing move is used, must have required combo points
# This ensures that when a finishing move s is selected at time t, the combo points k_{t}
# must be at least equal to the combo points consumed by that skill
constr -x_{t,s} * kappa_{s} * psi_{s} + M_{0} - M_{0} * x_{t,s} >= k_{t} * psi_{s},
  forall s in S, forall t in T

# Initial combo points constraint (at t=0)
# Set initial combo points based on skills used at start
constr k_{t} = sum_{s in S}{kappa_{s} * x_{t,s}},
  forall t in T bigcap {0}

# Initial energy constraint (at t=0)
# Set starting energy level plus energy from first skill
constr e_{t} = delta_{0} + sum_{s in S}{epsilon_{s} * x_{t,s}},
  forall t in T bigcap {0}

end

import elytica
import pandas
import json

# Global variables for model data
data = None
steps = None

def h(t, T):
    """Helper function to generate time range from t to T"""
    return json.dumps([i for i in range(int(t), int(T))])

def getSkills():
    """Return list of skill IDs (indices into data frame)"""
    return json.dumps([i for i in range(0, len(data["Name"].to_list()))])

def getDamage():
    """Return base damage values for all skills"""
    return json.dumps(data["BaseDamage"].to_list())

def getEnergy():
    """Return energy cost/gain for all skills (negative values = cost)"""
    return json.dumps(data["Energy"].to_list())

def getComboPoints():
    """Return combo points generated/consumed by each skill (negative = consumed)"""
    return json.dumps(data["ComboPoints"].to_list())

def getCooldowns():
    """Return cooldown duration for each skill"""
    return json.dumps(data["Cooldown"].to_list())

def getFinishingMove():
    """Return indicator: 1 if skill is a finishing move, 0 otherwise"""
    return json.dumps(data["FinishingMove"].to_list())

def getTime():
    """Return list of time steps in the rotation"""
    return json.dumps([i for i in range(0, steps)])

def main():
    global data, steps

    # Configuration
    steps = 5  # Number of time steps to optimize

    # Load skill data from CSV
    data = pandas.read_csv(
        "rogue.csv",
        usecols=["Name", "BaseDamage", "ComboPoints", "Energy", "Cooldown",
                 "FinishingMove", "Visible", "IncreasedDamage", "DurationIncreasedDamage"]
    )

    # Initialize and solve the optimization model
    elytica.init_model("max_damage")
    elytica.run_model("max_damage")

    print("\n" + "="*70)
    print("MMORPG SKILL ROTATION OPTIMIZATION SOLUTION")
    print("="*70)
    print("\nOptimized Rotation:")
    print("-"*70)

    # Collect and display the optimal rotation
    rotation = []
    for t in range(0, steps):
        energy = elytica.get_variable_value("max_damage", f"e{t}")
        combopoints = elytica.get_variable_value("max_damage", f"k{t}")

        for s in range(0, len(data["Name"].to_list())):
            val = elytica.get_variable_value("max_damage", f"x{t},{s}")
            if val > 0:
                skill_name = data["Name"][s]
                damage = data["BaseDamage"][s]
                time_sec = t * 0.2  # Convert time step to seconds (assuming 0.2s GCD)

                rotation.append({
                    "time_step": t,
                    "time_sec": time_sec,
                    "skill": skill_name,
                    "damage": damage,
                    "energy": energy,
                    "combo_points": combopoints
                })

                print(f"  t={t} ({time_sec:.1f}s): {skill_name:20s} | "
                      f"Damage={damage:5.0f} | Energy={energy:5.1f} | CP={combopoints:.0f}")

    # Get objective value (total damage)
    total_damage = elytica.get_best_primal_bound("max_damage")

    print("-"*70)
    print(f"Total Damage: {total_damage:.2f}")
    print("="*70)

    # Prepare results for output
    results = {
        "objective_value": total_damage,
        "rotation": rotation,
        "time_steps": steps
    }

    elytica.write_results(json.dumps(results, indent=2))

    return 0


# Expected CSV file format (rogue.csv):
#
# Name,BaseDamage,ComboPoints,Energy,Cooldown,FinishingMove,Visible,IncreasedDamage,DurationIncreasedDamage
# Eviscerate1,495,-1,-35,0,1,1,0,0
# Eviscerate2,990,-2,-35,0,1,1,0,0
# Eviscerate3,1485,-3,-35,0,1,1,0,0
# Eviscerate4,1980,-4,-35,0,1,1,0,0
# Eviscerate5,2475,-5,-35,0,1,1,0,0
# MarkedForDeath,0,5,0,30,0,1,0,0
# Backstab,956,1,-35,0,0,1,0,0
# AutoAttack,7,0,0,1.8,0,1,0,0
# Idle,0,0,0,0,0,1,0,0
# SymbolsOfDeath,0,0,40,30,0,1,0.15,10
#
# Field descriptions:
# - Name: Skill name
# - BaseDamage: Base damage dealt by the skill
# - ComboPoints: Combo points generated (positive) or consumed (negative)
# - Energy: Energy cost (negative) or gain (positive)
# - Cooldown: Cooldown duration in seconds
# - FinishingMove: 1 if finishing move (requires combo points), 0 otherwise
# - Visible: 1 if skill should be displayed in UI
# - IncreasedDamage: Damage multiplier buff applied by skill
# - DurationIncreasedDamage: Duration of damage buff in seconds

Read this through MCP: read with path: "examples/mmorpg-rotation", or resource hlpl://examples/mmorpg-rotation.