Plain text source at cmww.lisp
(declaim (optimize (speed 3) (safety 0) (debug 1)))

(defpackage :cmww
  (:use :cl :bordeaux-threads :alexandria)
  (:shadowing-import-from #:enhanced-eval-when #:eval-when)
  (:export :run-for-sim-config :report-results :make-sim-config
	   :*default-config-vals* :*sim-config*
	   :*skills* :skill-idx :*dmg-types* :dmg-type-idx
	   :*sim-totals* :avg))

(in-package :cmww)

(eval-when t
  (setf *read-default-float-format* 'double-float))

(setf *random-state* (make-random-state t))

;;;; utility functions

(defmacro while (test &rest body)
  `(do ()
       ((not ,test))

(defmacro in (obj &rest choices)
  (let ((insym (gensym)))
    `(let ((,insym ,obj))
       (or ,@(mapcar #'(lambda (c) `(eql ,insym ,c))

(defmacro aif (test-form then-form &optional else-form)
  `(let ((it ,test-form))
     (if it ,then-form ,else-form)))

(defun map-int (fn n)
  (declare (function fn)
	   (fixnum n))
  (let ((acc nil))
    (dotimes (i n)
      (declare (fixnum i))
      (push (funcall fn i) acc))
    (nreverse acc)))

(defmacro random-integer (max)
  `(the fixnum
	(random (the fixnum ,max))))

(defmacro random-ratio ()
  `(the double-float
	(random 1.0)))

(eval-when t
  (defun list-of-n-elts (elt n)
    (declare (fixnum n))
    (let ((list nil))
      (dotimes (i n)
	(push elt list))

(eval-when t
  (defun set-hash-entries (key-val-list table)
    (dolist (entry key-val-list)
      (destructuring-bind (key val) entry
	(setf (gethash key table) val)))

(eval-when t
  (defun all-hash-keys (table)
    (let ((result nil))
      (maphash (lambda (key val)
		 (declare (ignore val))
		 (push key result))
      (nreverse result))))

(defun sum-of-arrays (array-list element-type initial-value)
  (declare (optimize (speed 1) (safety 1) (debug 1)))
  (assert (every (lambda (array)
		   (= (length array) (length (first array-list))))
  (let* ((len (length (first array-list)))
	 (new-array (make-array len
				:element-type element-type
				:initial-element initial-value)))
    (dotimes (elt-idx len)
      (dolist (array array-list)
	(incf (aref new-array elt-idx)
	      (aref array elt-idx))))

(defmacro integer-op (op &rest args)
  `(the fixnum
	(,op ,@(mapcar (lambda (arg)
			 `(the fixnum ,arg))
(defmacro integer+ (&rest args)
  `(integer-op + ,@args))
(defmacro integer- (&rest args)
  `(integer-op - ,@args))
(defmacro integer* (&rest args)
  `(integer-op * ,@args))
(defmacro integer-min (&rest args)
  `(integer-op min ,@args))
(defmacro integer-max (&rest args)
  `(integer-op max ,@args))

;;;; end of utility functions

;; represents state of a single simulation
(eval-when t
  (defstruct sim-state
    (twisters nil :type list)
    (blasts nil :type list)

    (next-twisters-blasts-clear 0 :type fixnum)
    (next-storm-armor-hit 0 :type fixnum)
    (next-erosion-cast 0 :type fixnum)
    (next-follower-hit 0 :type fixnum)
    (next-ap-regen 0 :type fixnum)

    (effect-expire-frames 0 :type (simple-array fixnum))
    (skill-cd-expire-frames nil :type (simple-array fixnum))

    (current-ap 0.0 :type double-float)
    (current-cc-resist 0.0 :type double-float)

    (frame 0 :type fixnum)
    (command-idx 0 :type fixnum)
    (next-command-frame 0 :type fixnum)))

(eval-when t
  (defparameter *sim-state-field-symbols*
    '(:twisters :blasts :effect-expire-frames
      :skill-cd-expire-frames :current-ap :current-cc-resist
      :frame :command-idx :next-command-frame
      :next-twisters-blasts-clear :next-storm-armor-hit
      :next-erosion-cast :next-follower-hit :next-ap-regen)))

;; special variable bound to a table of configuration values
;; while running a set of simulations
(eval-when t
  (defvar *sim-config* (make-hash-table)))

;; special variable bound to a struct containing all recorded data
;; while running a set of simulations
(eval-when t
  (defvar *sim-totals* nil))

;; special variable bound to a struct representing current simulation state
;; while running a single simulation
(eval-when t
  (defvar *sim-state* nil))

;; list of castable skills
(eval-when t
  (declaim (list *skills*))
  (defparameter *skills*
    '(:diamond-skin :frost-nova :explosive-blast :energy-twister :weapon :slow-time)))

(eval-when t
  (declaim (fixnum *num-skills*))
  (defparameter *num-skills* (length *skills*)))

(eval-when t
  (defmacro skill-idx (skill)
    `(position ,skill *skills* :test #'eq)))

(eval-when t
  (defmacro skill-idx! (skill)
    (position skill *skills* :test #'eq)))

;; list of sources that damage can come from
(eval-when t
  (declaim (list *dmg-types*))
  (defparameter *dmg-types*
    '(:energy-twister :explosive-blast :storm-armor :shocking-aspect
      :follower :weapon :storm-crow :diamond-shards)))

(eval-when t
  (declaim (fixnum *num-dmg-types*))
  (defparameter *num-dmg-types* (length *dmg-types*)))

(eval-when t
  (defun dmg-type-idx (dmg-type)
    (position dmg-type *dmg-types* :test #'eq)))

(eval-when t
  (defmacro dmg-type-idx! (dmg-type)
    (position dmg-type *dmg-types* :test #'eq)))

;; used as the default configuration values
;; unless overriden by values specified in a custom configuration
(eval-when t
  (defparameter *default-config-vals*
    `((:num-targets 1)
      (:ww-targets-hit 1)
      (:simulation-seconds 35)
      (:num-simulations 250)
      (:frames-per-second 60)
      (:avg-command-interval 4.25)
      (:random-commands nil)
      (:command-skip-chance 0.0)
      (:cast-chain-reaction t)
      (:cast-frost-nova t)
      (:chill-always-active nil)
      (:using-diamond-shards nil)
      (:skip-windup-seconds 0)

      (:nova-before-ww-prob 0.0)
      (:permute-sequence t)
      (:simultaneous-commands t)
      (:enable-weapon-attack t)
      (:using-storm-crow nil)
      (:storm-crow-proc-rate 0.2)
      (:storm-crow-weap-dmg 0.7)
      (:storm-crow-cd-seconds 4.0)
      (:enable-nova-before-ww-prob t)

      (:max-effective-cc-resist 0.7)
      (:energy-twister-weap-dmg 2.52)
      (:explosive-blast-weap-dmg 0.97)
      (:diamond-shards-weap-dmg 2.10)
      (:energy-twister-ap-cost 35)
      (:explosive-blast-ap-cost 20)
      (:storm-armor-weap-dmg 1.0)
      (:variable-storm-armor-aps t)
      (:storm-armor-aps 1.0)
      (:storm-armor-aps-ratio 0.5)
      (:intel 2122)
      (:crit-chance 0.545)
      (:crit-damage 2.63)
      (:bonus-min-dmg 83)
      (:bonus-max-dmg 425)
      (:bonus-elem-dmg 0.0)
      (:weap-min-dmg-total 470)
      (:weap-max-dmg-total 871)
      (:weap-min-dmg-elem 301)
      (:weap-max-dmg-elem 560)
      (:weap-aps 1.63)
      (:armor-speed-bonus 0.55)
      (:bonus-vs-elites 0.0)
      (:apoc 19)
      (:bonus-max-ap 14)
      (:ap-regen-bonus 0)
      (:life-on-hit 0)
      (:life-steal 0.0)
      (:ww-crit-bonus 0.0)
      (:cr-crit-bonus 0.0)

      (:using-evocation t)
      (:using-cold-blooded t)
      (:using-glass-cannon nil)
      (:using-bone-chill t)
      (:using-cold-snap nil)
      (:using-storm-armor t)
      (:using-shocking-aspect t)
      (:using-energy-armor nil)
      (:using-pinpoint-barrier nil)
      (:using-force-weapon nil)
      (:using-blood-magic nil)
      (:using-stretch-time nil)
      (:using-time-warp nil)
      (:spam-slow-time nil)
      (:ap-cost-reduction 0)

      (:energy-twister-proc-rate 0.125)
      (:explosive-blast-proc-rate ,(/ 1 9.0))
      (:frost-nova-proc-rate ,(/ 1 6.0))
      (:diamond-shards-proc-rate ,(/ 1 6.0))
      (:weapon-attack-proc-rate 1.5)
      (:cm-proc-modifier 2.0)
      (:apoc-proc-modifier 1.15)
      (:sa-proc-modifier 2.5)

      (:follower-deals-damage t)
      (:follower-dps 0)
      (:using-scoundrel-crit-bonus nil)
      (:using-enchantress-speed-bonus nil)
      (:using-enchantress-erosion nil))))

(eval-when t
  (defparameter *independent-config-vars*
    (mapcar #'first *default-config-vals*)))

;; compute values which are constant over all simulations
;; and dependent on the specified configuration variables
(eval-when t
  (defun generate-dependent-sim-config-vars (config)
    (declare (optimize (speed 1) (safety 1) (debug 1)))
    (flet ((c (var)
	     (gethash var config))
	   (s (var val)
	     (setf (gethash var config) val)))
      (s :using-slow-time
	 (or (c :using-time-warp)
	     (c :using-stretch-time)))

      (s :windup-end-frame
	 (round (* (c :frames-per-second)
		   (c :skip-windup-seconds))))

      (s :seconds-recorded
	 (- (c :simulation-seconds)
	    (c :skip-windup-seconds)))

      ;; sequence of skills to be spammed
      (s :command-sequence
	  (list :diamond-skin
		(if (c :cast-chain-reaction) :explosive-blast nil)
		(if (c :cast-frost-nova) :frost-nova nil)
		(if (c :using-slow-time) :slow-time nil)

      ;; precompute all permutations of the command sequence
      (s :command-sequence-permutations
	 (let ((permutations nil))
	   (map-permutations (lambda (p)
			       (push p permutations))
			     (c :command-sequence))
	   (make-array (length permutations)
		       :element-type 'list
		       :initial-contents permutations)))

      (s :bonus-random-dmg
	 (- (c :bonus-max-dmg) (c :bonus-min-dmg)))

      (s :using-energy-armor
	 (or (c :using-energy-armor)
	     (c :using-pinpoint-barrier)))

      (s :crit-chance
	 (+ (c :crit-chance)
	    (if (c :using-pinpoint-barrier)
		0.05 0.0)
	    (if (c :using-scoundrel-crit-bonus)
		0.03 0.0)))
      (s :max-ap
	 (+ 100.0
	    (c :bonus-max-ap)
	    (if (c :using-energy-armor) -20 0)))
      (s :total-frames
	 (* (c :simulation-seconds)
	    (c :frames-per-second)))

      ;; passive AP regen
      (s :ap-regen-per-second
	 (+ 10.0 (c :ap-regen-bonus)))
      (s :ap-regen-per-frame
	 (float (/ (c :ap-regen-per-second)
		   (c :frames-per-second))))
      ;; convert specified weapon damage values into values more suitable for calculations
      (s :weap-random-dmg-total
	 (- (c :weap-max-dmg-total)
	    (c :weap-min-dmg-total)))
      (s :weap-random-dmg-elem
	 (- (c :weap-max-dmg-elem)
	    (c :weap-min-dmg-elem)))
      (s :weap-min-dmg-phys
	 (- (c :weap-min-dmg-total)
	    (c :weap-min-dmg-elem)))
      (s :weap-random-dmg-phys
	 (- (c :weap-random-dmg-total)
	    (c :weap-random-dmg-elem)))
      ;; combine physical damage coming from weapon and offhand/jewelry
      (s :total-min-dmg-phys
	 (+ (c :weap-min-dmg-phys)
	    (c :bonus-min-dmg)))
      (s :total-random-dmg-phys
	 (+ (c :weap-random-dmg-phys)
	    (c :bonus-random-dmg)))

      ;; character sheet APS
      (s :aps
	 (* (+ (c :weap-aps)
	       (if (c :using-enchantress-speed-bonus)
		   0.03 0.0))
	    (+ 1.0
	       (c :armor-speed-bonus)
	       (if (c :using-stretch-time)
		   0.10 0.0))))

      ;; global cooldown triggered by energy twister, frost nova, slow time
      (s :global-cooldown-frame-length
	 (round (* (/ (c :frames-per-second) 60)
		   (floor (/ 60 (c :aps))))))
      ;; multiplier to damage provided by intelligence
      (s :intel-dps-multiplier
	 (+ 1.0 (/ (c :intel) 100.0)))
      ;; multiplier to average damage provided by crit-chance and crit-damage
      (s :crit-dps-multiplier
	 (let ((cc (c :crit-chance))
	       (cd (c :crit-damage)))
	   (+ (* (- 1.0 cc) 1.0)
	      (* cc (+ 1.0 cd)))))

      ;; calculate DPS shown on weapon
      (s :weap-dps
	 (* (+ (+ (c :weap-min-dmg-phys)
		  (/ (c :weap-random-dmg-phys) 2.0))
	       (+ (c :weap-min-dmg-elem)
		  (/ (c :weap-random-dmg-elem) 2.0)))
	    (c :weap-aps)))
      ;; calculate character sheet DPS value
      (s :char-dps
	 (* (+ (* (+ (c :total-min-dmg-phys)
		     (/ (c :total-random-dmg-phys) 2.0))
		  (+ 1.0 (c :bonus-elem-dmg)))
	       (+ (c :weap-min-dmg-elem)
		  (/ (c :weap-random-dmg-elem) 2.0)))
	    (c :intel-dps-multiplier)
	    (c :crit-dps-multiplier)
	    (c :aps)
	    (+ 1.0
	       (if (c :using-glass-cannon)
		   0.15 0.0)
	       (if (c :using-force-weapon)
		   0.15 0.0)
	       (if (c :using-blood-magic)
		   0.10 0.0))))

      ;; energy twister tick rate based on forum thread
      (s :energy-twister-ticks-per-cast
	 (ceiling (/ 360 (floor (/ 30 (c :aps))))))
      ;; frames per tick with a 6 second twister duration
      (s :energy-twister-frames-per-tick
	 (floor (/ (* 6 (c :frames-per-second))
		   (c :energy-twister-ticks-per-cast))))
      (s :energy-twister-weap-dmg-per-tick
	 (/ (c :energy-twister-weap-dmg)
	    (c :energy-twister-ticks-per-cast)))
      (s :diamond-skin-frame-length
	 (* 6 (c :frames-per-second)))

      ;; time between chain reaction cast and first blast
      (s :explosive-blast-initial-delay
	 (round (* 1.5 (c :frames-per-second))))
      ;; time between chain reaction blasts
      (s :explosive-blast-delay-after-blast
	 (round (* 0.5 (c :frames-per-second))))

      ;; create array of skill cooldown lengths
      (s :skill-cooldown-frame-lengths
	 (let ((skill-cooldown-frame-lengths
		(make-array *num-skills* :initial-element 0 :element-type 'fixnum)))
	   (dolist (entry `((:explosive-blast 6)
			    (:diamond-skin 15)
			    (:frost-nova ,(if (c :using-cold-snap) 9 12))
			    (:slow-time 15)))
	     (destructuring-bind (skill seconds) entry
	       (setf (elt skill-cooldown-frame-lengths (skill-idx skill))
		     (round (* seconds
			       (c :frames-per-second)
			       (if (c :using-evocation) 0.85 1.0))))))

      (s :storm-crow-cooldown-frame-length
	 (round (* (c :storm-crow-cd-seconds) (c :frames-per-second))))

      ;; number of frames used as fixed frequency for Storm Armor base spell hits
      (s :storm-armor-frames-per-hit
	 (if (c :variable-storm-armor-aps)
	     (round (/ (c :frames-per-second)
		       (* (c :aps) (c :storm-armor-aps-ratio))))
	     (round (/ (c :frames-per-second)
		       (c :storm-armor-aps)))))

      ;; create array of skill proc rates
      (s :skill-proc-rates
	 (let ((skill-proc-rates (make-array *num-skills*
					     :initial-element 0.0
					     :element-type 'double-float)))
	   (dolist (entry `((:diamond-skin ,(if (c :using-diamond-shards)
						(c :diamond-shards-proc-rate)
			    (:frost-nova ,(c :frost-nova-proc-rate))
			    (:explosive-blast ,(c :explosive-blast-proc-rate))
			    (:energy-twister ,(c :energy-twister-proc-rate))
			    (:weapon ,(c :weapon-attack-proc-rate))))
	     (destructuring-bind (skill proc-rate) entry
	       (setf (elt skill-proc-rates (skill-idx skill))
		     (coerce (float proc-rate) 'double-float))))

      ;; ap-cost-reduction is applied to each skill here
      (s :skill-ap-costs
	 (let ((skill-ap-costs (make-array *num-skills*
					   :initial-element 0.0
					   :element-type 'double-float)))
	   (dolist (entry `((:energy-twister ,(c :energy-twister-ap-cost))
			    (:explosive-blast ,(c :explosive-blast-ap-cost))))
	     (destructuring-bind (skill ap-cost) entry
	       (setf (elt skill-ap-costs (skill-idx skill))
		     (coerce (float (max 0.0 (- ap-cost (c :ap-cost-reduction))))

      ;; pre-compute average damage dealt by hits to save cpu time
      (s :avg-hit-dmg
	 (let ((phys-dmg (+ (c :total-min-dmg-phys)
			    (* (c :total-random-dmg-phys)
	       (elem-dmg (+ (c :weap-min-dmg-elem)
			    (* (c :weap-random-dmg-elem)
	   (* (+ elem-dmg (* phys-dmg (+ 1.0 (c :bonus-elem-dmg))))
	      (c :intel-dps-multiplier)
	      (+ 1.0 (c :bonus-vs-elites))
	      (+ 1.0
		 (if (c :using-glass-cannon)
		     0.15 0.0)
		 (if (c :using-force-weapon)
		     0.15 0.0)
		 (if (c :using-blood-magic)
		     0.10 0.0)))))

      ;; frequency at which certain stats are recorded
      (s :frames-per-stats-record
	 (round (/ (c :frames-per-second) 10)))

      ;; frequency at which passive AP regeneration occurs
      (s :frames-per-ap-regen
	 (round (/ (c :frames-per-second) 10)))

      ;; this is to correct for the way the balance between DPS and Frost Nova casts
      ;; in simulation results universally swings towards Frost Nova casts as APS increases
      (s :nova-before-ww-prob
	 (if (c :enable-nova-before-ww-prob)
	     (* 1.0 (expt (/ 2.35 (c :aps)) 4))

;; used to generate compile-time type declarations
(eval-when t
  (defparameter *integer-config-vars*
    '(:num-targets :simulation-seconds :num-simulations
      :frames-per-second :skip-windup-seconds 
      :intel :windup-end-frame :seconds-recorded :total-frames 
      :energy-twister-ticks-per-cast :energy-twister-frames-per-tick
      :explosive-blast-initial-delay :explosive-blast-delay-after-blast
      :storm-crow-cooldown-frame-length :storm-armor-frames-per-hit 
      :frames-per-stats-record :frames-per-ap-regen)))
(eval-when t
  (defparameter *float-config-vars*
    '(:bonus-max-ap :max-ap :apoc :ap-cost-reduction
      :energy-twister-ap-cost :explosive-blast-ap-cost
      :life-on-hit :ww-targets-hit :avg-command-interval
      :command-skip-chance :nova-before-ww-prob :storm-crow-proc-rate
      :storm-crow-weap-dmg :storm-crow-cd-seconds
      :max-effective-cc-resist :energy-twister-weap-dmg :explosive-blast-weap-dmg
      :diamond-shards-weap-dmg :storm-armor-weap-dmg :storm-armor-aps
      :storm-armor-aps-ratio :crit-chance :crit-damage :bonus-min-dmg
      :bonus-max-dmg :bonus-elem-dmg :weap-min-dmg-total :weap-max-dmg-total
      :weap-min-dmg-elem :weap-max-dmg-elem :weap-aps :armor-speed-bonus
      :bonus-vs-elites :ap-regen-bonus :life-steal
      :ww-crit-bonus :cr-crit-bonus
      :energy-twister-proc-rate :explosive-blast-proc-rate :frost-nova-proc-rate
      :diamond-shards-proc-rate :weapon-attack-proc-rate 
      :cm-proc-modifier :apoc-proc-modifier :sa-proc-modifier :follower-dps
      :bonus-random-dmg :ap-regen-per-second :ap-regen-per-frame
      :weap-random-dmg-total :weap-random-dmg-elem :weap-min-dmg-phys :weap-random-dmg-phys
      :total-min-dmg-phys :total-random-dmg-phys :aps
      :intel-dps-multiplier :crit-dps-multiplier :weap-dps :char-dps
      :energy-twister-weap-dmg-per-tick :avg-hit-dmg)))

;; creates a hash table containing all configuration input
(eval-when t
  (defun make-sim-config (config-vals &optional override-config-vals)
    (declare (optimize (speed 1) (safety 1) (debug 1)))
    (let ((config (make-hash-table)))
      (set-hash-entries config-vals config)
      (unless (null override-config-vals)
	(set-hash-entries override-config-vals config))
      (generate-dependent-sim-config-vars config)
      (dolist (var *integer-config-vars*)
	(setf (gethash var config)
	      (the fixnum (round (gethash var config)))))
      (dolist (var *float-config-vars*)
	(setf (gethash var config)
	      (coerce (float (gethash var config)) 'double-float)))

;; bind all fields of *sim-config* to special variables
;; to provide direct access without hash table lookups
(eval-when t
  (defmacro generate-special-variable-defvars (var-list)
    (let ((var-list (eval var-list)))
	 ,@(mapcar (lambda (var-sym)
		     (let ((special-var-sym (read-from-string
					     (format nil "*~A*" (symbol-name var-sym)))))
		       `(defvar ,special-var-sym nil)))
(eval-when t
  (defmacro with-vals-bound-to-special-variables (var-list &body body)
    (let ((var-list (eval var-list)))
      `(let (,@(mapcar (lambda (var-sym)
			 (let ((special-var-sym (read-from-string
						 (format nil "*~A*" (symbol-name var-sym)))))
			   `(,special-var-sym (gethash ,var-sym *sim-config*))))
(eval-when t
  (setf *sim-config* (make-sim-config *default-config-vals*)))
(eval-when t
  (defparameter *all-config-vars* (all-hash-keys *sim-config*)))
(eval-when t
  (generate-special-variable-defvars *all-config-vars*))

(eval-when t
  (defparameter *integer-array-config-vars*
    (remove-if-not (lambda (var-sym)
		     (let ((val (gethash var-sym *sim-config*)))
		       (and (arrayp val)
			    (integerp (aref val 0)))))
(eval-when t
  (defparameter *float-array-config-vars*
    (remove-if-not (lambda (var-sym)
		     (let ((val (gethash var-sym *sim-config*)))
		       (and (arrayp val)
			    (floatp (aref val 0)))))

;; used to access variables in *sim-config* and *sim-state*
(eval-when t
  (defmacro v (var-sym &optional array-idx)
    (let ((is-state-field (member var-sym *sim-state-field-symbols*))
	  (var-name (symbol-name var-sym)))
      (if is-state-field
	  ;; access *sim-state* fields through struct accessor
	  (let ((accessor-sym (read-from-string (format nil "sim-state-~A" var-name))))
	    (if (null array-idx)
		`(,accessor-sym *sim-state*)
		`(aref (,accessor-sym *sim-state*) (the fixnum ,array-idx))))
	  ;; access *sim-config* fields through special variable
	  (let ((special-var-sym (read-from-string (format nil "*~A*" var-name))))
	      ;; insert compile-time type declarations based on variable type
	      ((member var-sym *integer-config-vars*)
	       `(the fixnum ,special-var-sym))
	      ((member var-sym *float-config-vars*)
	       `(the double-float ,special-var-sym))
	      ((and array-idx
		    (member var-sym *integer-array-config-vars*))
	       `(the fixnum (aref (the (simple-array fixnum) ,special-var-sym)
				  (the fixnum ,array-idx))))
	      ((and array-idx
		    (member var-sym *float-array-config-vars*))
	       `(the double-float (aref (the (simple-array double-float) ,special-var-sym)
					(the fixnum ,array-idx))))
	       `(aref (the simple-array ,special-var-sym) (the fixnum ,array-idx)))

;; effects are active when their expiration frame (in *sim-state*) is in the future
(eval-when t
  (declaim (list *sim-effects*))
  (defparameter *sim-effects*
    '(:freeze :erosion :global-cd :diamond-skin :slow-time :erosion-cd :storm-crow-cd)))

;; uptime is recorded for these effects
(eval-when t
  (declaim (list *sampled-effects*))
  (defparameter *sampled-effects*
    '(:freeze :erosion :global-cd)))

(eval-when t
  (declaim (fixnum *num-effects*))
  (defparameter *num-effects* (length *sim-effects*)))

(eval-when t
  (defmacro effect-idx (name)
    `(position ,name *sim-effects* :test #'eq)))

(eval-when t
  (defmacro effect-idx! (name)
    (position name *sim-effects* :test #'eq)))

;; stores all data recorded while simulating
(eval-when t
  (defstruct sim-totals
    (damage 0.0 :type double-float)
    (apoc-gain 0.0 :type double-float)
    (ap-spent 0.0 :type double-float)
    (life-on-hit 0.0 :type double-float)
    (life-steal 0.0 :type double-float)
    (cm-procs-from-skill nil :type (simple-array fixnum))
    (damage-from-source nil :type (simple-array double-float))
    (skill-casts nil :type (simple-array fixnum))
    (sample-frames 0 :type fixnum)
    (arcane-power 0.0 :type double-float)
    (active-twisters 0 :type fixnum)
    (effect-active nil :type (simple-array fixnum))
    (num-simulations 0 :type fixnum)))

;; used to access fields of *sim-totals*
(defmacro total (var-sym &optional value-idx)
  (if (null value-idx)
      `(,(read-from-string (format nil "cmww::sim-totals-~A" var-sym))
      `(aref (,(read-from-string (format nil "cmww::sim-totals-~A" var-sym))

(defmacro add-to-total (var-sym value &optional value-idx)
  `(unless (in-windup)
     (incf (total ,var-sym ,value-idx) ,value)))

(eval-when t
  (defparameter *sim-totals-sampled-fields*
    '(:arcane-power :active-twisters :effect-active)))

;; compute an average value using *sim-totals* and the appropriate denominator
(defmacro avg (var-sym &optional value-idx)
  (with-gensyms (denom)
    `(let ((,denom ,(if (member var-sym *sim-totals-sampled-fields*)
			`(total :sample-frames)
			`(* (gethash :seconds-recorded *sim-config*)
			    (total :num-simulations)))))
       (float (/ (total ,var-sym ,value-idx)

(defun make-initial-sim-totals ()
   (make-array *num-skills* :element-type 'fixnum :initial-element 0)
   (make-array *num-dmg-types* :element-type 'double-float :initial-element
   (make-array *num-skills* :element-type 'fixnum :initial-element 0)
   (make-array *num-effects* :element-type 'fixnum :initial-element 0)))

;; used to combine multiple sim-totals structs from different threads
(defun combine-sim-totals (totals-list)
  (declare (optimize (speed 1) (safety 1) (debug 1)))
  (flet ((combine-single-field (accessor-fn)
	   (reduce #'+ (mapcar accessor-fn totals-list)))
	 (combine-array-field (accessor-fn element-type initial-value)
	   (sum-of-arrays (mapcar accessor-fn totals-list)
			  element-type initial-value)))
     :damage (combine-single-field #'sim-totals-damage)
     :apoc-gain (combine-single-field #'sim-totals-apoc-gain)
     :ap-spent (combine-single-field #'sim-totals-ap-spent)
     :life-on-hit (combine-single-field #'sim-totals-life-on-hit)
     :life-steal (combine-single-field #'sim-totals-life-steal)
     :cm-procs-from-skill (combine-array-field #'sim-totals-cm-procs-from-skill
					       'fixnum 0)
     :damage-from-source (combine-array-field #'sim-totals-damage-from-source
					      'double-float 0.0)
     :skill-casts (combine-array-field #'sim-totals-skill-casts
				       'fixnum 0)
     :sample-frames (combine-single-field #'sim-totals-sample-frames)
     :arcane-power (combine-single-field #'sim-totals-arcane-power)
     :active-twisters (combine-single-field #'sim-totals-active-twisters)
     :effect-active (combine-array-field #'sim-totals-effect-active
					 'fixnum 0)
     :num-simulations (combine-single-field #'sim-totals-num-simulations))))

(defun make-initial-sim-state ()
   (make-array *num-effects* :element-type 'fixnum :initial-element 0)
   (make-array *num-skills* :element-type 'fixnum :initial-element 0)
   (v :max-ap)))

;;;; main simulation logic starts here

(defmacro in-windup ()
  `(< (v :frame) (v :windup-end-frame)))

(defmacro add-to-arcane-power (amount)
  `(setf (v :current-ap)
	 (min (v :max-ap)
	      (+ (v :current-ap) ,amount))))

(defmacro spend-arcane-power (amount)
  (with-gensyms (amount-val)
    `(let ((,amount-val ,amount))
       (decf (v :current-ap) ,amount-val)
       (add-to-total :ap-spent ,amount-val)

(defmacro effect-active (effect-idx)
  `(< (v :frame) (v :effect-expire-frames ,effect-idx)))

(defmacro effect-active! (name)
  `(< (v :frame) (v :effect-expire-frames ,(effect-idx name))))

(defmacro effect-expire-frame! (name)
  `(v :effect-expire-frames ,(effect-idx name)))

;; remove slow time from the command sequence when this returns true
(defmacro waiting-to-spam-slow-time ()
  `(and (v :using-slow-time)
	(not (v :spam-slow-time))
	(> (integer- (effect-expire-frame! :slow-time)
		     (v :frame))
	   (integer* 3 (v :frames-per-second)))))

;; trigger LoH and life steal
(declaim (inline trigger-life-gain-procs))
(defun trigger-life-gain-procs (skill-idx targets-hit total-dmg)
  (declare (fixnum skill-idx targets-hit)
	   (double-float total-dmg))
  (unless (= skill-idx -1)
    (add-to-total :life-on-hit
		  (* (v :life-on-hit)
		     (v :skill-proc-rates skill-idx)
		     (coerce targets-hit 'double-float))))
  (add-to-total :life-steal
		(* total-dmg
		   (+ (v :life-steal)
		      (if (v :using-blood-magic)
			  0.015 0.0))

(defmacro skill-cooldown-is-active (skill-idx)
  `(< (v :frame) (v :skill-cd-expire-frames ,skill-idx)))

;; current multiplier to damage from temporary effects
(defmacro current-dmg-multiplier (dmg-from-player)
  `(* (+ 1.0
	 (if (and ,dmg-from-player
		  (v :using-cold-blooded)
		  (or (effect-active! :freeze)
		      (v :chill-always-active)))
	     0.20 0.0))
      (+ 1.0
	 (if (and (v :using-bone-chill)
		  (effect-active! :freeze))
	     0.15 0.0)
	 (if (and (v :using-enchantress-erosion)
		  (effect-active! :erosion))
	     0.15 0.0)
	 (if (v :using-time-warp)
	     0.20 0.0))))

;; calculates average damage dealt by a hit to a single target from a skill dealing
;; weap-dmg-multiplier weapon damage per hit, with current damage buffs applied
(defmacro random-hit-dmg (weap-dmg-multiplier is-crit)
  `(* (v :avg-hit-dmg)
      (if ,is-crit
	  (+ 1.0 (v :crit-damage))
      (current-dmg-multiplier t)))

;; apply a single hit (record damage, activate all procs)
;; fixed-dmg is used for follower hits
;; skill-idx is -1 for hits that don't generate procs
(declaim (inline do-hit))
(defun do-hit (&key (weap-dmg 0.0) (num-targets 1) (crit-chance 0.0)
		 (skill-idx -1) (dmg-type-idx -1) (fixed-dmg 0.0))
  (declare (fixnum num-targets skill-idx dmg-type-idx)
	   (double-float crit-chance weap-dmg fixed-dmg))
  (let* ((is-crit (if (= crit-chance 0.0)
		      (< (random-ratio) crit-chance)))
	 (hit-dmg (if (= weap-dmg 0.0)
		      (* (random-hit-dmg weap-dmg is-crit)
    (add-to-total :damage hit-dmg)
    (add-to-total :damage-from-source hit-dmg dmg-type-idx)
    (unless (= weap-dmg 0.0)
      (trigger-life-gain-procs skill-idx num-targets hit-dmg))
    (when (and is-crit (/= skill-idx -1))
      (activate-procs-on-crit skill-idx num-targets))))

(defmacro do-shocking-aspect-hit ()
  `(do-hit :weap-dmg 0.35
	   :num-targets 1
	   :crit-chance (v :crit-chance)
	   :skill-idx -1
	   :dmg-type-idx (dmg-type-idx! :shocking-aspect)))

(defmacro activate-apoc (skill-proc-rate targets-hit)
  (with-gensyms (ap)
    `(let ((,ap (* (v :apoc)
		   (v :apoc-proc-modifier)
       (add-to-arcane-power ,ap)
       (add-to-total :apoc-gain ,ap)

(defun activate-critical-mass (trigger-skill-idx)
  (declare (fixnum trigger-skill-idx))
  (dotimes (skill-idx *num-skills*)
    (when (< (v :frame)
	     (v :skill-cd-expire-frames skill-idx))
      (setf (v :skill-cd-expire-frames skill-idx)
            (integer-max (v :frame)
			 (integer- (v :skill-cd-expire-frames skill-idx)
				   (v :frames-per-second))))))
  (add-to-total :cm-procs-from-skill 1 trigger-skill-idx)

;; randomly activate APoC,CM,SA for a critical hit
(defun activate-procs-on-crit (trigger-skill-idx targets-hit)
  (declare (fixnum trigger-skill-idx targets-hit))
  (let ((skill-proc-rate (v :skill-proc-rates trigger-skill-idx)))
    (activate-apoc skill-proc-rate targets-hit)
    (dotimes (i targets-hit)
      (when (< (random-ratio)
	       (* skill-proc-rate (v :cm-proc-modifier)))
	(activate-critical-mass trigger-skill-idx))
      (when (and (v :using-shocking-aspect)
		 (< (random-ratio)
		    (* skill-proc-rate (v :sa-proc-modifier))))

;; list of twisters after removing expired twisters
(defun active-twisters ()
  (setf (v :twisters)
	(delete-if-not (lambda (twister)
			 (< (twister-num-ticks-applied twister)
			    (v :energy-twister-ticks-per-cast)))
		       (v :twisters))))

;; list of blasts after removing expired blasts
(defun active-blasts ()
  (setf (v :blasts)
	(delete-if-not (lambda (blast)
			 (< (blast-num-hits-applied blast) 3))
		       (v :blasts))))

(defmacro trigger-skill-cooldown (skill-idx)
  (with-gensyms (skill-idx-sym)
    `(let ((,skill-idx-sym ,skill-idx))
       (setf (v :skill-cd-expire-frames ,skill-idx-sym)
	     (integer+ (v :frame)
		       (v :skill-cooldown-frame-lengths ,skill-idx-sym))))))

(defmacro activate-effect (name expire-frame)
  `(setf (v :effect-expire-frames ,(effect-idx name))

(defmacro trigger-global-cooldown ()
  `(activate-effect :global-cd
		    (integer+ (v :frame)
			      (v :global-cooldown-frame-length))))

(defmacro skill-has-cooldown (skill)
  `(in ,skill :diamond-skin :frost-nova :explosive-blast :slow-time))

(defmacro skill-uses-global-cooldown (skill)
  `(in ,skill :energy-twister :frost-nova :slow-time :weapon))

;; storm crow fireball proc
(defun trigger-storm-crow-proc-maybe ()
  (when (and (v :using-storm-crow)
	     (not (effect-active! :storm-crow-cd))
	     (< (random-ratio) (v :storm-crow-proc-rate)))
    (do-hit :weap-dmg (v :storm-crow-weap-dmg)
	    :num-targets 1
	    :crit-chance (v :crit-chance)
	    :skill-idx -1
	    :dmg-type-idx (dmg-type-idx! :storm-crow))
    (activate-effect :storm-crow-cd
		     (integer+ (v :frame)
			       (v :storm-crow-cooldown-frame-length)))

(defstruct twister
  (next-tick-frame 0 :type fixnum)
  (num-ticks-applied 0 :type fixnum)
  (num-targets 1 :type fixnum))

(defstruct blast
  (next-hit-frame 0 :type fixnum)
  (num-hits-applied 0 :type fixnum))

;; immediately cast skill if able to do so
(defun cast-skill (skill)
  (let ((skill-idx (skill-idx skill)))
    ;; do nothing if cooldown prevents casting
    (unless (or (skill-cooldown-is-active skill-idx)
		(and (skill-uses-global-cooldown skill)
		     (effect-active! :global-cd)))
	;; cast skill if have enough AP
	((> (v :current-ap)
	    (v :skill-ap-costs skill-idx))
	 (add-to-total :skill-casts 1 skill-idx)
	 (let ((ap-cost (v :skill-ap-costs skill-idx)))
	   (when (> ap-cost 0.0)
	     (spend-arcane-power ap-cost)))
	 (when (skill-has-cooldown skill)
	   (trigger-skill-cooldown skill-idx))
	 (when (skill-uses-global-cooldown skill)
	 ;; perform skill-specific cast actions
	 (case skill
	    (activate-effect :slow-time
			     (integer+ (v :frame)
				       (integer* 8 (v :frames-per-second)))))
	    (when (and (v :using-diamond-shards)
		       (effect-active! :diamond-skin))
	      (do-hit :weap-dmg (v :diamond-shards-weap-dmg)
		      :num-targets (v :num-targets)
		      :crit-chance (v :crit-chance)
		      :skill-idx (skill-idx! :diamond-skin)
		      :dmg-type-idx (dmg-type-idx! :diamond-shards)))
	    (activate-effect :diamond-skin
			     (integer+ (v :frame)
				       (v :diamond-skin-frame-length))))
	    ;; apply 3 second freeze to all targets by rules of 1.0.4 Crowd Control Changes post
            (let* ((freeze-expire-frame
		    (effect-expire-frame! :freeze))
		    (min (v :max-effective-cc-resist)
			 (v :current-cc-resist)))
		    (integer+ (v :frame)
			      (round (* 3.0
					(v :frames-per-second)
					(- 1.0 effective-cc-resist))))))
	      ;; if this freeze extends beyond the existing freeze
	      (when (> new-freeze-expire-frame freeze-expire-frame)
		(let* ((additional-freeze-frames
			(if (> freeze-expire-frame (v :frame))
			    (integer- new-freeze-expire-frame
			    (integer- new-freeze-expire-frame
				      (v :frame))))
			(coerce (float (/ (the fixnum additional-freeze-frames)
					       (v :frames-per-second)))
		  ;; increase current CC resistance by 0.1 per second
		  (setf (v :current-cc-resist)
			(min 1.0 (+ (v :current-cc-resist)
				    (* 0.1 additional-freeze-seconds))))
		  (activate-effect :freeze new-freeze-expire-frame))))
	    (when (< (random-ratio) (v :crit-chance))
	      (activate-procs-on-crit (skill-idx! :frost-nova) (v :num-targets))))
	    (let ((blast
		   (make-blast :next-hit-frame
			       (integer+ (v :frame)
					 (v :explosive-blast-initial-delay))
	      ;; add blast to list of active blasts
	      (push blast (v :blasts))))
	    (multiple-value-bind (base-target-count extra-target-chance)
		(floor (v :ww-targets-hit))
	      (declare (fixnum base-target-count)
		       (double-float extra-target-chance))
	      (let* ((num-targets (integer+ base-target-count
					    (if (< (random-ratio) extra-target-chance)
						1 0)))
		      (make-twister :next-tick-frame (1+ (v :frame))
				    :num-ticks-applied 0
				    :num-targets num-targets)))
		;; add twister to list of active twisters
		(push twister (v :twisters))))
	    (do-hit :weap-dmg 1.0
		    :num-targets 1
		    :crit-chance (v :crit-chance)
		    :skill-idx (skill-idx! :weapon)
		    :dmg-type-idx (dmg-type-idx! :weapon))
	    (error (format nil "invalid skill: ~A" skill)))))
	;; skill cast failed from insufficient AP
	((and (v :enable-weapon-attack)
	      (eql skill :energy-twister))
	 (cast-skill :weapon))))))

;; record values for stats which are sampled per-frame
(defmacro record-sampled-stats ()
  `(unless (in-windup)
     (when (= 0 (random-integer (v :frames-per-stats-record)))
       (add-to-total :sample-frames 1)
       ,@(map 'list
	      (lambda (effect-name)
		(let ((effect-idx (effect-idx effect-name)))
		  `(when (effect-active ,effect-idx)
		     (add-to-total :effect-active 1 ,effect-idx))))
       (add-to-total :arcane-power (v :current-ap))
       (add-to-total :active-twisters (list-length (active-twisters))))))

;; randomly generate a number of frames to delay before issuing next skill commands;
;; gives an average equal to the avg-command-interval parameter
(defun random-command-interval ()
  (let ((min-ratio (/ 3.0 5.0)))
      (+ (* (v :avg-command-interval)
	 (* (v :avg-command-interval)
	    (- 1.0 min-ratio)

;; attempts to cast every skill in the command sequence;
;; used when simultaneous-commands option is enabled
(defun send-all-skill-commands ()
  (let* ((seq (if (v :permute-sequence)
		  ;; randomly select a pre-computed permutation
		  (v :command-sequence-permutations
		      (length (v :command-sequence-permutations))))
		  (v :command-sequence)))
	 (seq (if (waiting-to-spam-slow-time)
		  (remove :slow-time seq)
    (declare (list seq))
    (if (< (v :command-skip-chance) 0.01)
	;; use faster simplified loop when command-skip-chance is disabled
	(dolist (skill seq)
	  (when (and (eql skill :energy-twister)
		     (v :cast-frost-nova)
		     (> (v :nova-before-ww-prob) 0.005)
		     (< (random-ratio) (v :nova-before-ww-prob)))
	    (cast-skill :frost-nova))
	  (cast-skill skill))
	;; full algorithm supporting command-skip-chance
	(let ((skills-attempted nil))
	  (while (some (lambda (skill)
			 (not (member skill skills-attempted :test #'eq)))
	    (dolist (skill seq)
	      (unless (member skill skills-attempted :test #'eq)
		(unless (< (random-ratio) (v :command-skip-chance))
		  (when (and (eql skill :energy-twister)
			     (v :cast-frost-nova)
			     (> (v :nova-before-ww-prob) 0.005)
			     (< (random-ratio) (v :nova-before-ww-prob)))
		    (push :frost-nova skills-attempted)
		    (cast-skill :frost-nova))
		  (push skill skills-attempted)
		  (cast-skill skill)))))))))

;; attempts to cast the next single skill in the command sequence;
;; used when simultaneous-commands option is disabled
(defun send-next-single-skill-command ()
  (let* ((seq (if (waiting-to-spam-slow-time)
		  (remove :slow-time (v :command-sequence))
		  (v :command-sequence)))
	 (skill (if (v :random-commands)
		    (nth (random-integer (list-length seq)) seq)
		    (nth (mod (v :command-idx)
			      (list-length seq))
    ;; advance current position in command sequence
    (incf (v :command-idx))
    ;; randomly skip one command in the sequence
    (when (< (random-ratio) (v :command-skip-chance))
      (incf (v :command-idx)))

    (cast-skill skill)))

;; main simulation loop
(defun run-single-simulation ()
  (let ((*sim-state* (make-initial-sim-state)))
    (while (< (v :frame) (v :total-frames))
      ;; periodically remove entries for expired twisters and blasts
      (when (= (v :frame) (v :next-twisters-blasts-clear))
	(setf (v :twisters) (active-twisters))
	(setf (v :blasts) (active-blasts))
	(incf (v :next-twisters-blasts-clear)
	      (v :frames-per-second)))

      ;; apply hits from active twisters
      (dolist (twister (v :twisters))
	(when (and (= (v :frame)
		      (twister-next-tick-frame twister))
		   (< (twister-num-ticks-applied twister)
		      (v :energy-twister-ticks-per-cast)))
	  (do-hit :weap-dmg (v :energy-twister-weap-dmg-per-tick)
		  :num-targets (twister-num-targets twister)
		  :crit-chance (+ (v :crit-chance) (v :ww-crit-bonus))
		  :skill-idx (skill-idx! :energy-twister)
		  :dmg-type-idx (dmg-type-idx! :energy-twister))
	  (incf (twister-next-tick-frame twister)
		(v :energy-twister-frames-per-tick))
	  (incf (twister-num-ticks-applied twister))))

      ;; apply hits from active blasts
      (dolist (blast (v :blasts))
	(when (and (= (v :frame) (blast-next-hit-frame blast))
		   (< (blast-num-hits-applied blast) 3))
	  (do-hit :weap-dmg (v :explosive-blast-weap-dmg)
		  :num-targets (v :num-targets)
		  :crit-chance (+ (v :crit-chance) (v :cr-crit-bonus))
		  :skill-idx (skill-idx! :explosive-blast)
		  :dmg-type-idx (dmg-type-idx! :explosive-blast))
	  (incf (blast-next-hit-frame blast)
		(v :explosive-blast-delay-after-blast))
	  (incf (blast-num-hits-applied blast))))

      ;; reduce CC resistance for unfrozen targets by 0.1 per second
      (when (not (effect-active! :freeze))
	(setf (v :current-cc-resist)
	      (max 0.0
		   (- (v :current-cc-resist)
		      (/ 0.1 (v :frames-per-second))))))

      ;; apply storm armor hit at fixed interval
      (when (and (v :using-storm-armor)
		 (= (v :frame) (v :next-storm-armor-hit)))
	(do-hit :weap-dmg (v :storm-armor-weap-dmg)
		:num-targets 1
		:crit-chance (v :crit-chance)
		:skill-idx -1
		:dmg-type-idx (dmg-type-idx! :storm-armor))
	(incf (v :next-storm-armor-hit)
	      (v :storm-armor-frames-per-hit)))

      ;; cast enchantress erosion one second after cooldown expires
      (when (and (v :using-enchantress-erosion)
		 (= (v :frame) (v :next-erosion-cast)))
	(activate-effect :erosion
			 (integer+ (v :frame)
				   (integer* 3 (v :frames-per-second))))
	(activate-effect :erosion-cd
			 (integer+ (v :frame)
				   (integer* 15 (v :frames-per-second))))
	(incf (v :next-erosion-cast)
	      (integer* 16 (v :frames-per-second))))

      ;; apply damage from follower once per second
      (when (and (v :follower-deals-damage)
		 (= (v :frame) (v :next-follower-hit)))
	(do-hit :weap-dmg 0.0
		:num-targets 1
		:crit-chance 0.0
		:skill-idx -1
		:dmg-type-idx (dmg-type-idx! :follower)
		:fixed-dmg (* (v :follower-dps)
			      (current-dmg-multiplier nil)))
	(incf (v :next-follower-hit)
	      (v :frames-per-second)))

      (when (= (v :frame) (v :next-command-frame))
	;; set next frame in which to issue commands
	(incf (v :next-command-frame) (random-command-interval))
        ;; issue skill command(s)
	(if (v :simultaneous-commands)

      ;; passive AP regeneration
      (when (= (v :frame) (v :next-ap-regen))
	(add-to-arcane-power (* (v :ap-regen-per-frame)
				(v :frames-per-ap-regen)))
	(incf (v :next-ap-regen)
	      (v :frames-per-ap-regen)))

      ;; record values for stats which are sampled per-frame
      ;; advance to next frame
      (incf (v :frame)))
    (add-to-total :num-simulations 1)

;; generate a string describing simulation results;
;; with 'sim-config-only' will return only the values available prior to simulation
(defun report-results (&key (sim-config-only nil) (html nil))
  (declare (optimize (speed 1) (safety 1) (debug 1)))
  (with-vals-bound-to-special-variables *all-config-vars*
    (flet ((n-spaces (n)
	     (map 'string (lambda (n)
			    (declare (ignore n))
		  (map-int #'identity n))))
      (with-output-to-string (stream)
	(format stream "Fighting ~A target(s) for ~As~A - ~A samples~%"
		(v :num-targets)
		(- (v :simulation-seconds)
		   (v :skip-windup-seconds))
		(if (> (v :skip-windup-seconds) 0.05)
		    (format nil " (after ~As windup)" (v :skip-windup-seconds))
		(v :num-simulations))
	(format stream "Average seconds between commands = ~6,4Fs (~4,2F frames with FPS=~A)~%"
		(float (/ (v :avg-command-interval)
			  (v :frames-per-second)))
		(v :avg-command-interval)
		(v :frames-per-second))
	(format stream "APS = ~4,4F [~A WW ticks per cast] ; Crit Chance = ~4,2F% ; APoC = ~A~%"
		(v :aps) (v :energy-twister-ticks-per-cast)
		(* 100 (v :crit-chance))
		(round (v :apoc)))
	(if html
	    (format stream "Character sheet DPS = <span class='chardps'>~2,2F</span> ; Weapon DPS = ~4,2F~%"
		    (v :char-dps) (v :weap-dps))
	    (format stream "Character sheet DPS = ~2,2F ; Weapon DPS = ~4,2F~%"
		    (v :char-dps) (v :weap-dps)))
	(format stream "----------------------------------------------------------------------~%")
	(unless sim-config-only
	  (if html
	      (if (> (v :num-targets) 1)
		  (format stream "Average DPS = <span class='truedps'>~,2F</span> [total over all targets]~%"
			  (avg :damage))
		  (format stream "Average DPS = <span class='truedps'>~,2F</span>~%"
			  (avg :damage)))
	      (format stream "Average DPS = ~,2F~%"
		      (avg :damage)))
	  (format stream "(DPS / Character Sheet) Ratio = ~4,2F~%"
		  (/ (avg :damage)
		     (v :char-dps)))
	  (dolist (entry '((:energy-twister "Wicked Wind")
			   (:explosive-blast "Chain Reaction")
			   (:diamond-shards "Diamond Shards")
			   (:storm-armor "Storm Armor base")
			   (:shocking-aspect "Shocking Aspect")
			   (:weapon "weapon attack")
			   (:storm-crow "Storm Crow proc")
			   (:follower "follower")))
	    (destructuring-bind (dmg-type name) entry
	      (let ((dps-from-source
		     (avg :damage-from-source (dmg-type-idx dmg-type))))
		(when (or (eql dmg-type :weapon)
			  (> dps-from-source 0.1))
		  (let* ((str-0 (format nil "DPS from ~A" name))
			 (str-1 (format nil "~A~A= ~,2F"
					(n-spaces (- 27 (length str-0)))
			 (final-str (format nil "~A~A[~5,2F%]"
					    (n-spaces (- 42 (length str-1)))
					    (* (/ dps-from-source
						  (avg :damage))
		    (format stream "~A~%" final-str))))))
	(format stream "Average freeze uptime = ~4,2F%~%"
		(* 100 (avg :effect-active (effect-idx :freeze))))
	(let ((casts-per-second
	       (avg :skill-casts (skill-idx :frost-nova))))
	  (when (> casts-per-second 0.0)
	    (format stream "Average seconds per Frost Nova cast = ~4,2F seconds~%"
		    (/ 1.0 casts-per-second)))
	  (format stream "Average Frost Nova casts per fight = ~4,2F~%"
		  (* casts-per-second (v :seconds-recorded))))
	(let ((casts-per-second
	       (avg :skill-casts (skill-idx :diamond-skin))))
	  (when (> casts-per-second 0.0)
	    (format stream "Average seconds per Diamond Skin cast = ~4,2F seconds~%"
		    (/ 1.0 casts-per-second)))
	  (format stream "Average Diamond Skin casts per fight = ~4,2F~%"
		  (* casts-per-second (v :seconds-recorded))))
	(let ((casts-per-second
	       (avg :skill-casts (skill-idx :slow-time))))
	  (when (> casts-per-second 0.01)
	    (format stream "Average seconds per Slow Time cast = ~4,2F seconds~%"
		    (/ 1.0 casts-per-second))))
	(when (v :using-enchantress-erosion)
	  (format stream "Average Enchantress Erosion uptime = ~4,2F%~%"
		  (* 100 (avg :effect-active (effect-idx :erosion)))))
	(format stream "Average Wicked Wind casts per second = ~4,2F~%"
		(avg :skill-casts (skill-idx :energy-twister)))
	(format stream "Average active twisters = ~4,2F~%"
		(avg :active-twisters))
	(format stream "Average Chain Reaction casts per second = ~4,2F~%"
		(avg :skill-casts (skill-idx :explosive-blast)))
	(format stream "Average global cooldown uptime = ~4,2F%~%"
		(* 100 (avg :effect-active (effect-idx :global-cd))))
	(let* ((total-rate
		(reduce #'+ (map-int (lambda (skill-idx)
				       (avg :cm-procs-from-skill skill-idx))
		(avg :cm-procs-from-skill
			 (skill-idx :energy-twister))))
	  (format stream "Average CM procs per second = ~4,2F (~4,2F% from WW)~%"
		  (if (> total-rate 0.0)
		      (* 100 (/ ww-rate total-rate))
	(format stream "Average AP gained per second = ~4,2F~%"
		(+ (avg :apoc-gain)
		   (v :ap-regen-per-second)))
	(format stream "Average AP spent per second = ~4,2F~%"
		(avg :ap-spent))
	(format stream "Average Arcane Power = ~4,2F (out of max ~A)~%"
		(avg :arcane-power)
		(round (v :max-ap)))
	(let ((avg-life-gain (+ (avg :life-on-hit)
				(avg :life-steal))))
	  (format stream "Average life gain per second = ~4,2F (~4,2F% from LoH)"
		  (if (< avg-life-gain 0.01)
		      (* (float (/ (avg :life-on-hit)

;; run a set of simulations according to the given configuration
;; and return a string describing the results
(defun run-for-sim-config (config &key (return-string t) (html nil) (num-threads 1))
  (declare (optimize (speed 1) (safety 1) (debug 1)))
  (let ((num-threads (min num-threads (gethash :num-simulations config))))
    (if (= num-threads 1)
	;; single-threaded version
	(let* ((*sim-config* config)
	       (*sim-totals* (make-initial-sim-totals)))
	  (with-vals-bound-to-special-variables *all-config-vars*
	    (dotimes (i (v :num-simulations))
	    (if return-string
		(report-results :html html)
	;; multi-threaded version
	(let* ((thread-totals (list-of-n-elts nil num-threads))
		 (lambda (n)
		    (lambda ()
		      (setf (elt thread-totals n)
			    (let ((*random-state* (make-random-state t))
				  (*sim-config* config))
			      (with-vals-bound-to-special-variables *all-config-vars*
				(let ((*num-simulations*
				       (floor (/ (v :num-simulations) num-threads)))
				      (*sim-totals* (make-initial-sim-totals)))
				  (dotimes (i (v :num-simulations))
		    :name (format nil "run-for-sim-config #~A" n)))
		(mapcar #'join-thread threads))
		(combine-sim-totals thread-totals)))
	  (declare (ignore join-result))
	  (if return-string
	      (let ((*sim-config* config)
		    (*sim-totals* combined-totals))
		(report-results :html html))

(defun cmww-time-test (num-threads)
    (make-sim-config *default-config-vals* `((:num-simulations 1000)
					     (:frames-per-second 100)
					     (:simultaneous-commands t)
					     (:avg-command-interval 12.5)))
    :num-threads num-threads))

(eval-when t
  (require 'sb-sprof))

(defun cmww-profile-test ()
  (sb-sprof:with-profiling (:max-samples 5000 :report :flat :loop nil)
     (make-sim-config *default-config-vals* `((:num-simulations 5000)
					      (:frames-per-second 100)
					      (:simultaneous-commands t)
					      (:avg-command-interval 12.5))))))