#!/usr/bin/python
import math
# Model constants.
#
# These can be adjusted to tune to adjust the way the model works, but the
# default values are strongly recommended. Instead it is better to adjust
# the actor's static attributes. In particular the staminaReturn and
# staminaBurn attributes affect how quickly stamina recovers and burns.
# The max sustainable lift in bodyweights.
weightMult = 1.0
# The factors affecting the fraction of muscles required for lifting.
liftExp = 2.0
liftMult = (0.5 * weightMult)**liftExp
# The factors affecting the fraction of muscles damaged by wounds.
woundExp = 2.0
woundMult = 1.0
# The factors affecting the energy cost of performing actions.
runMult = 1.0 # running cost per moveMax distance.
walkMult = 4.0 # walking cost per moveMax distance at moveMax speed.
climbMult = 2.0 # climbing up cost per climbMax vertical distance.
downMult = climbMult/5.0 # climbing down cost per climbMax vertical distance.
jumpMult = 1.0 # jumping cost per jumpMax vertical distance.
# The climb rate low-pass filter timeconstant.
climbRC = 0.2 # Average vertical movement over 0.2 seconds.
# The acceleration from gravity.
g = 9.81 # Used for calculating jumpSpeeds from jumpHeights.
class Actor(object):
""" An Actor.
Attributes:
E: Encumbrance as a fraction of max sustainable lift (0.0-2.0).
H: Health as a fraction of max health (0.0-1.0).
S: Stamina as a fraction of max stamina (0.0-1.0).
staminaReturn: Fraction of stamina tha recovers each second.
staminaBurn: Fraction of stamina burned each second.
moveMax: Max flat running speed in m/s.
climbMax: Max vertical climbing speed in m/s.
jumpMax: Max vertical jump height in m.
moveEffortMult: desired movement effort fraction.
isStanding: Indicates the actor is not sitting/laying.
isRunning: Indicates the actor is running.
isWalking: Indicates the actor is walking.
moveSpeed: The actors movement speed in m/s.
jumpSpeed: The actors vertical takeoff velocity after jumping.
Properties:
M: The actors total weight in bodyweights.
Smin: The minimum stamina required from encumbrance.
Smax: The maximum stamina from wounds.
"""
def __init__(self,
E=0.0, H=1.0, S=1.0,
staminaReturn=0.04, staminaBurn=0.04,
moveMax=12.0, climbMax=3.0, jumpMax=1.5):
"""Initialise an Actor."""
# Actor's dynamic attributes.
self.E = E
self.H = H
self.S = S
# Actor's static stamina burn/return rates.
self.staminaBurn = staminaBurn
self.staminaReturn = staminaReturn
# Actor's static movement ability limits.
self.moveMax = moveMax
self.climbMax = climbMax
self.jumpMax = jumpMax
# Actors desired move effort fraction.
self.moveEffortMult = 0.0
# Actor's movement state attributes.
self.isStanding = True # True if on the ground and not sitting/laying.
self.isRunning = False # True if on the ground and running.
self.isWalking = False # True if on the ground and walking.
# Actor's low-pass filtered climb rate.
self.climbRate = 0.0
# Actor's target movement speed.
self.moveSpeed = 0.0
self.jumpSpeed = 0.0
@property
def Smax(self):
"""Calculate the max stamina limit from wounds."""
return 1.0 - woundMult * (1.0-self.H)**woundExp
@property
def Smin(self):
"""Calculate the min stamina required to stand from encumbrance."""
return liftMult * self.E**liftExp
@property
def M(self):
"""Calculate the actors total weight in bodyweights from encumbrance."""
return 1.0 + weightMult*self.E
def _updateClimbRate(self, dx, dy, dz, dt):
"""Calculate and low-pass filter climbRate from change in position.
This is measured in fraction of vertical climbMax distance moved per
moveMax total distance moved.
"""
dd = math.sqrt(dz*dz + dx*dx + dy*dy)
if (dd > 0.0) and (self.isRunning or self.isWalking):
climbRateNow = (dz / dd) * (self.moveMax/self.climbMax)
else:
climbRateNow = 0.0
self.climbRate = (dt*climbRateNow + climbRC*self.climbRate)/(dt + climbRC)
def setStats(self, E=None, H=None, S=None):
"""Set the actors enumbrance, health, and/or stamina.
Note S will be adjusted to take into account changes in H to keep
the same S/Smax ratio.
"""
if E is not None: self.E = E
if H is not None:
oldSmax = self.Smax
self.H = H
self.S += (self.Smax - oldSmax) * self.S/oldSmax
if S is not None: self.S = S
def setState(self, move=0.0, standing=True):
"""Set the actors movement effort and standing state.
Set move=1.0 for running, move=0.16 for walking.
Set standing false when sitting or lying down.
"""
self.moveEffortMult = move
self.isStanding = standing
def updateFrame(self, dx, dy, dz, dt):
"""Update attributes for a frame."""
self._updateClimbRate(dx, dy, dz, dt)
# Calc climbing and moving scaling factors from climbRate.
slopeClimbMult = max(self.climbRate, -self.climbRate*downMult/climbMult)
slopeMoveMult = 1.0/(self.climbRate**2 + 1.0)
slopeSpeedMult = 1.0/(slopeClimbMult + slopeMoveMult)
# Calc the running/walking speed multipliers.
runSpeedMult = self.moveEffortMult*(self.S-self.Smin)/self.M
walkSpeedMult = math.sqrt(runMult/walkMult * runSpeedMult)
# Calc the running/walking state by comparing movement speeds.
self.isRunning = runSpeedMult > walkSpeedMult
self.isWalking = not self.isRunning and walkSpeedMult > 0.0
# Calc the fraction of muscles used for lifting.
if self.isStanding:
Slift = self.Smin
else:
Slift = 0.0
# Calc move speed scale and fraction of muscles used for moving.
if self.isRunning:
moveScale = slopeSpeedMult * runSpeedMult
Smove = slopeMoveMult * runMult * self.M * moveScale
elif self.isWalking:
moveScale = slopeSpeedMult * walkSpeedMult
Smove = slopeMoveMult * walkMult * self.M * moveScale**2
else:
moveScale = 0.0
Smove = 0.0
# Calc the fraction of muscles used for climbing.
Sclimb = slopeClimbMult * climbMult * self.M * moveScale
# Calc fraction of undamaged, fatigued muscles that are resting.
Srest = (self.Smax - self.S)
# Calc the moveSpeed and adjust S for the frame.
self.moveSpeed = moveScale * self.moveMax
self.S += (self.staminaReturn*Srest - self.staminaBurn*(Slift + Smove + Sclimb)) * dt
def doJump(self, height=1.0):
"""Perform a jump."""
jumpScale = height * (self.S - self.Smin)/self.M
jumpHeight = jumpScale * self.jumpMax
self.jumpSpeed = math.sqrt(2*g*jumpHeight)
self.S -= self.staminaBurn * jumpMult * self.M * jumpScale
if __name__ == '__main__':
angle = 90.0 * math.pi/180.0
a = Actor(staminaBurn=0.0165, staminaReturn=0.013, climbMax=2.0, jumpMax=1.2)
a.setStats(E=0.0)
a.setState(move=1.0)
i = 0
x,y,z,t = 0.0,0.0,0.0,0.0
dt = 0.1
while t < 2*60.0:
if i % 10 == 0:
print "%7.1f:(%6.2f,%5.2f) S=%5.3f v=%5.2f %s" % (t, x, z, a.S, a.moveSpeed, a.isRunning)
dx = a.moveSpeed * math.cos(angle) * dt
dy = 0.0
dz = a.moveSpeed * math.sin(angle) * dt
a.updateFrame(dx,dy,dz,dt)
i+=1
x+=dx
y+=dy
z+=dz
t+=dt