myspace / Lua /Tactical /AnimationGraph.lua
sirnii's picture
Upload 1816 files
b6a38d7 verified
raw
history blame
22.6 kB
-- not specified animations continue to "Idle"
local s_TransitionRules = {
["%s_Standing_Idle"] = {
{ "%s_Standing_Run", moment = "any", last = true },
{ "%s_Standing_Walk", moment = "any", last = true },
{ "%s_Standing_CombatRun", moment = "any", last = true },
{ "%s_Standing_CombatWalk", moment = "any", last = true },
{ "%s_Standing_To_Crouch", moment = "any" },
{ "%s_Standing_To_Prone", moment = "any" },
{ "%s_Standing_Aim", moment = "any", last = true },
{ "%s_Standing_ExposeLeft_Start", moment = "any", },
{ "%s_Standing_ExposeRight_Start", moment = "any", },
{ "gr_Standing_Aim", moment = "any", last = true },
},
["%s_Crouch_Idle"] = {
{ "%s_Standing_Run", moment = "any", last = true },
{ "%s_Standing_Walk", moment = "any", last = true },
{ "%s_Standing_CombatRun", moment = "any", last = true },
{ "%s_Standing_CombatWalk", moment = "any", last = true },
{ "%s_Crouch_To_Standing", moment = "any" },
{ "%s_Crouch_To_Prone", moment = "any" },
{ "%s_TakeCover_Idle", moment = "any" },
{ "gr_Standing_Aim", moment = "any" },
{ "nw_Bandaging_Start", moment = "any" },
},
["%s_Prone_Idle"] = {
{ "%s_Prone_To_Standing", moment = "any" },
{ "%s_Prone_To_Crouch", moment = "any" },
},
["%s_Downed_Idle"] = {
{ "%s_Downed_Standing", moment = "any" },
{ "%s_Downed_Crouch", moment = "any" },
{ "%s_Downed_Prone", moment = "any" },
},
["%s_Standing_Run"] = {
{ "%s_Standing_Idle", moment = "any" },
},
["%s_Standing_Walk"] = {
{ "%s_Standing_Idle", moment = "any" },
},
["%s_Standing_CombatRun"] = {
{ "%s_Standing_Idle", moment = "any" },
},
["%s_Standing_CombatWalk"] = {
{ "%s_Standing_Idle", moment = "any" },
},
["%s_Standing_Aim"] = {
{ "%s_Standing_Idle", moment = "any" },
},
["%s_Standing_Aim_Forward"] = {
{ "%s_Standing_Idle", moment = "any" },
},
["%s_Standing_Aim_Down"] = {
{ "%s_Standing_Idle", moment = "any" },
},
["%s_Standing_Fire"] = {
{ "%s_Standing_Idle", moment = "any" },
},
["gr_Standing_Aim"] = {
{ "%s_Standing_Idle", moment = "any" },
{ "%s_Crouch_Idle", moment = "any" },
},
["nw_Bandaging_Start"] = { { "%s_Crouch_Idle" } },
["nw_Bandaging_Self_Start"] = { { "%s_Crouch_Idle" } },
["nw_Bandaging_Idle"] = { { "%s_Crouch_Idle" } },
["nw_Bandaging_Self_Idle"] = { { "%s_Crouch_Idle" } },
["nw_Bandaging_End"] = { { "%s_Crouch_Idle" } },
["nw_Bandaging_Self_End"] = { { "%s_Crouch_Idle" } },
["nw_Standing_MortarIdle"] = {
{ "nw_Standing_MortarEnd", moment = "any" },
},
["nw_Standing_MortarFire"] = {
{ "nw_Standing_MortarEnd", moment = "any" },
},
["nw_Standing_MortarEnd"] = {
{ "%s_Standing_Idle" },
},
["%s_Standing_To_Crouch"] = { { "%s_Crouch_Idle" } },
["%s_Standing_To_Prone"] = { { "%s_Prone_Idle" } },
["%s_Crouch_To_Standing"] = { { "%s_Standing_Idle" } },
["%s_Crouch_To_Prone"] = { { "%s_Prone_Idle" } },
["%s_TakeCover_Idle"] = { { "%s_Crouch_Idle", moment = "any" }},
["%s_Prone_To_Standing"] = { { "%s_Standing_Idle" } },
["%s_Prone_To_Crouch"] = { { "%s_Crouch_Idle" } },
["%s_Downed_Standing"] = { { "%s_Standing_Idle" } },
["%s_Downed_Crouch"] = { { "%s_Crouch_Idle" } },
["%s_Downed_Prone"] = { { "%s_Prone_Idle" } },
["%s_Open_Door"] = {
{ "%s_Standing_Idle", moment = "any" },
{ "%s_Crouch_Idle", moment = "any" },
},
["%s_CloseDoor"] = {
{ "%s_Standing_Idle", moment = "any" },
{ "%s_Crouch_Idle", moment = "any" },
},
["%s_Standing_ExposeLeft_Start"] = {
{ "%s_Standing_ExposeLeft_Idle" },
locked_orientation = true,
},
["%s_Standing_ExposeRight_Start"] = {
{ "%s_Standing_ExposeRight_Idle" },
locked_orientation = true,
},
["%s_Standing_ExposeLeft_Idle"] = {
{ "%s_Standing_ExposeLeft_End", moment = "any" },
locked_orientation = true,
},
["%s_Standing_ExposeRight_Idle"] = {
{ "%s_Standing_ExposeRight_End", moment = "any" },
locked_orientation = true,
},
["%s_Standing_ExposeLeft_End"] = {
{ "%s_Standing_Idle" },
locked_orientation = true,
},
["%s_Standing_ExposeRight_End"] = {
{ "%s_Standing_Idle" },
locked_orientation = true,
},
}
function ResolveAnimationTransitions(prefixes)
local result = {}
for anim_format, transitions_format in sorted_pairs(s_TransitionRules) do
for j, prefix in ipairs(prefixes) do
local anim = string.format(anim_format, prefix)
local anim_transitions = result[anim]
if not anim_transitions then
anim_transitions = {
prefix = string.starts_with(anim, prefix) and prefix or string.match(anim, "^(%a+_).*"),
}
result[anim] = anim_transitions
end
if transitions_format.locked_orientation then
anim_transitions.locked_orientation = transitions_format.locked_orientation
end
for k, transition_format in ipairs(transitions_format) do
assert(s_TransitionRules[transition_format[1]])
local target_anim = string.format(transition_format[1], prefix)
local transition = table.copy(transition_format)
transition[1] = target_anim
local idx = table.find(anim_transitions, 1, target_anim) or #anim_transitions + 1
anim_transitions[idx] = transition
end
end
end
return result
end
s_AnimTransitions = ResolveAnimationTransitions({ "civ", "ar", "mk", "dw", "hg", "gr", "nw", "hw" })
local ValidStances = {
Standing = true,
Crouch = true,
Prone = true,
}
function GetAnimPath(obj, start_anim, target_anim)
if start_anim == target_anim then
return
end
local queue = { start_anim, [start_anim] = 0 }
local queue_transitions = { false }
if not s_AnimTransitions[start_anim] then
local prefix = string.match(start_anim, "^(%a+_)")
local stance = prefix and obj.species == "Human" and string.match(start_anim, "^%a+_(%a+)_")
if stance == "KnockDown" then
stance = "Prone"
end
local closest = stance and string.format("%s%s_Idle", prefix, stance)
if closest == target_anim then
return
end
if not s_AnimTransitions[closest] then
if obj.species == "Human" then
if not stance or not ValidStances[stance] then
stance = "Standing"
end
if prefix == "nw_" or prefix == "civ_" then
prefix = string.match(target_anim, "^(%a+_)") or obj:GetWeaponAnimPrefix()
end
closest = string.format("%s%s_Idle", prefix, stance)
if closest == target_anim then
return
end
end
if not s_AnimTransitions[closest] then
return
end
end
queue_transitions[2] = false
queue[2] = closest
queue[closest] = 1
end
local target_tokens = {}
local target_token_start = 1
while target_token_start <= #target_anim do
local index = string.find(target_anim, "_", target_token_start)
if not index then
break
end
table.insert(target_tokens, string.sub(target_anim, 1, index))
target_token_start = index + 1
end
if #target_tokens == 0 then
return
end
local target_prefix = string.sub(target_tokens[1], 1, #target_tokens[1] - 1)
local variation_index = string.match(target_anim, "()%d+$")
if variation_index then
table.insert(target_tokens, string.sub(target_anim, 1, variation_index - 1))
else
table.insert(target_tokens, target_anim)
end
local GetAnimDist = function(anim)
if anim == target_anim then
return 0
end
local dist = #target_tokens + 1
for i, token in ipairs(target_tokens) do
if not string.starts_with(anim, token) then
break
end
dist = dist - 1
end
if anim:ends_with("Idle") then
return dist
end
return dist + 1
end
local idx = #queue
local best_idx = idx
local best_dist = GetAnimDist(queue[idx])
local best_threshold = s_AnimTransitions[target_anim] and 0 or 1
while idx <= #queue and best_dist > best_threshold do
local anim = queue[idx]
local dist = GetAnimDist(anim)
if dist - best_dist < 2 then
local anim_data = s_AnimTransitions[anim]
-- invalid animations can be skipped when moment == "any"
local is_valid_anim = IsValidAnim(obj, anim)
for i, transition in ipairs(anim_data) do
local new_anim = transition[1]
local new_anim_data = s_AnimTransitions[new_anim] or empty_table
if not queue[new_anim]
and (is_valid_anim or transition.moment == "any")
and (anim_data.prefix == new_anim_data.prefix or new_anim_data.prefix == target_prefix)
then
queue[new_anim] = idx
local dist = GetAnimDist(new_anim)
if dist - best_dist < 2 then
if not transition.last then
table.insert(queue, new_anim)
table.insert(queue_transitions, transition)
end
if IsValidAnim(obj, new_anim) then
if dist < best_dist then
best_idx, best_dist = #queue, dist
end
if dist == 0 then
break
end
end
end
end
end
end
idx = idx + 1
end
local path = {}
idx = best_idx
local transition
while idx > 0 do
local anim = queue[idx]
if transition and transition.moment ~= "any" then
table.insert(path, anim)
end
transition = queue_transitions[idx]
idx = queue[anim]
end
if path[1] == obj:GetStateText() and obj:GetAnimPhase() >= GetAnimDuration(obj:GetEntity(), path[1]) - 1 then
table.remove(path, 1)
end
if #path == 0 then
return
end
return path
end
function PlayTransitionAnims(obj, target_anim, angle, aim_pos)
local start_anim = obj:GetStateText()
local path = GetAnimPath(obj, start_anim, target_anim)
if not path and obj.species == "Human" then
local prefix = string.match(target_anim, "^(%a+_)")
if prefix == "inf_" then
local nw_target_anim = "nw" .. string.sub(target_anim, #prefix)
return PlayTransitionAnims(obj, nw_target_anim, angle, aim_pos)
end
if prefix and not string.starts_with(start_anim, prefix) then
local stance = string.match(start_anim, "^%a+_(%a+)_")
if stance then
if stance == "Bandaging" then
stance = "Crouch"
end
local closest = string.format("%s%s_Idle", prefix, stance)
path = GetAnimPath(obj, closest, target_anim)
end
end
end
if not path then
return false
end
local len = #path
for i = len, 1, -1 do
local anim = path[i]
if i < len or obj:GetStateText() ~= anim then
obj:SetState(anim, const.eKeepComponentTargets)
end
if angle and not (s_AnimTransitions[anim] or empty_table).locked_orientation then
local angle_diff = AngleDiff(angle, obj:GetOrientationAngle())
if angle_diff ~= 0 then
local t = obj:TimeToMoment(1, "OrientationStart")
if (t or 0) > 0 then
Sleep(t)
end
t = obj:TimeToMoment(1, "OrientationEnd") or Min(300, obj:TimeToAnimEnd())
obj:IdleRotation(angle, t)
end
angle = nil
end
if i == 1 and aim_pos and anim:ends_with("Aim_Start") then
local t = obj:TimeToAnimEnd() - obj:GetAnimDuration() / 2 -- wait middle of the animation
if t > 0 then
Sleep(t)
end
obj:SetIK("AimIK", aim_pos)
Sleep(obj:TimeToAnimEnd())
end
Sleep(obj:TimeToAnimEnd())
end
return true
end
local function GetDieAnimSuffix(context, fall_angle)
local unit = context.unit
local diff = AngleDiff(unit:GetVisualAngle(), fall_angle)
if abs(diff) <= 45 * 60 then
return "_F", fall_angle
elseif abs(diff) >= 135 * 60 then
return "_B", fall_angle + 180 * 60
elseif diff < 0 then
return "_R", fall_angle - 90*60
end
return "_L", fall_angle + 90*60
end
local function CheckNearbyPass(pos, except_dir)
local offset = const.SlabSizeX / 2
for y = -1, 1 do
for x = -1, 1 do
local p = point(x * offset, y * offset, 0)
if not except_dir or abs(AngleDiff(CalcOrientation(p), except_dir)) >= 90*60 then
if not terrain.IsPassable(pos + p) then
return false
end
end
end
end
return true
end
-- TODO: here we can incorporate transitions to another anims
local s_ConditionsGraph = {
{
Name = "Death_Sitting",
Condition = function(context)
if context.hit_descr and context.hit_descr.die_pos then
local visit = context.unit and context.unit.last_visit
return IsValid(visit) and IsKindOf(visit, "AL_SitChair")
end
end,
Animation = function(context)
local dir_angle = CardinalDirection(context.angle)
local _, angle = GetDieAnimSuffix(context, dir_angle)
return "civ_DeathChair", true, context.hit_descr.die_pos, angle
end,
},
{
Name = "Death_Wall_NextSlab",
Condition = function(context)
if not context.close_shot_dist or context.move_slabs ~= 0 then
return false
end
if GetAngleCover(context.pos, CardinalDirection(context.angle)) ~= const.CoverHigh then
return false -- no cover
end
return true
end,
Animation = function(context)
local dir_angle = CardinalDirection(context.angle)
local suffix, angle = GetDieAnimSuffix(context, dir_angle)
local variations = CheckNearbyPass(context.pos, dir_angle)
return "civ_DeathWall" .. suffix, variations, context.pos, angle
end,
},
{
Name = "Death_Wall_OverNextSlab",
Condition = function(context)
if not context.close_shot_dist or not context.move_pos1 then
return false
end
local dx = context.move_pos1:x() - context.pos:x()
local dy = context.move_pos1:y() - context.pos:y()
if dx * dy ~= 0 then
return false -- diagonal move
end
if GetAngleCover(context.move_pos1, CardinalDirection(context.angle)) ~= const.CoverHigh then
return false -- no cover
end
return true
end,
Animation = function(context)
local dir_angle = CardinalDirection(context.angle)
local suffix, angle = GetDieAnimSuffix(context, dir_angle)
local variations = CheckNearbyPass(context.pos, dir_angle)
return "civ_DeathWall_x1" .. suffix, variations, context.move_pos1, angle
end,
},
{
Name = "Death_Fall",
Condition = function(context)
if not context.close_shot_dist or context.move_slabs ~= 0 then
return false
end
local tunnel = GetTunnelDir(context.pos, CardinalDirection(context.angle), const.TunnelMaskDrop)
if not tunnel then
return false
end
return true
end,
Animation = function(context)
local tunnel = GetTunnelDir(context.pos, CardinalDirection(context.angle), const.TunnelMaskDrop)
if not tunnel then
return
end
local suffix, angle = GetDieAnimSuffix(context, CardinalDirection(context.angle))
local anim = string.format("civ_DeathFall_x%d%s", tunnel.tiles, suffix)
return anim, true, tunnel:GetExit(), angle
end,
},
{
Name = "Death_Window",
Condition = function(context)
if not context.close_shot_dist or context.move_slabs ~= 0 then
return false
end
if (context.hit_descr.impact_force or 0) < 1 then
return false
end
local tunnel = GetTunnelDir(context.pos, CardinalDirection(context.angle), const.TunnelTypeWindow | const.TunnelTypeJumpOver1)
if not tunnel then
return false
end
local move_pos = tunnel:GetExit()
local next_pos = GetPassSlab(RotateRadius(const.SlabSizeX, context.angle, move_pos))
if not next_pos or not IsPassSlabStep(move_pos, next_pos, const.TunnelTypeWalk) then
return false -- annimations enter the next tile
end
return true
end,
Animation = function(context)
local tunnel = GetTunnelDir(context.pos, CardinalDirection(context.angle), const.TunnelTypeWindow | const.TunnelTypeJumpOver1)
if not tunnel then
return
end
local suffix, angle = GetDieAnimSuffix(context, CardinalDirection(context.angle))
local anim = "civ_DeathWindow" .. suffix
local break_obj
if tunnel.tunnel_type & const.TunnelTypeWindow ~= 0 then
local obj = tunnel.pass_through_obj
if obj.pass_through_state == "intact" then
break_obj = obj
end
end
return anim, true, tunnel:GetExit(), angle, break_obj
end,
},
{
Name = "Death_Railing",
Condition = function(context)
if not context.close_shot_dist or context.move_slabs ~= 0 then
return false
end
if (context.hit_descr.impact_force or 0) < 1 then
return false
end
local tunnel_mask = const.TunnelTypeWindow | const.TunnelTypeJumpOver1 | const.TunnelTypeJumpOver2
local tunnel = GetTunnelDir(context.pos, CardinalDirection(context.angle), tunnel_mask)
if not tunnel then
return false
end
return true
end,
Animation = function(context)
local angle = CardinalDirection(context.angle)
local tunnel_mask = const.TunnelTypeWindow | const.TunnelTypeJumpOver1 | const.TunnelTypeJumpOver2
local tunnel = GetTunnelDir(context.pos, angle, tunnel_mask)
if not tunnel then
return
end
local suffix, angle = GetDieAnimSuffix(context, angle)
local anim = "civ_DeathRailing" .. suffix
return anim, true, context.pos, angle
end,
},
{
Name = "Death_Over2Slabs",
Condition = function(context)
if not context.close_shot_dist or not context.move_pos3 then
return false
end
if (context.hit_descr.impact_force or 0) < 3 then
return false
end
return true
end,
Animation = function(context)
local suffix, angle = GetDieAnimSuffix(context, context.angle)
return "civ_DeathSlide" .. suffix, true, context.move_pos3, angle
end,
},
{
Name = "Death_OverNextSlab",
Condition = function(context)
-- the head is in the 3th tile
if not context.close_shot_dist or context.move_slabs < 3 then
return false
end
if (context.hit_descr.impact_force or 0) < 2 then
return false
end
return true
end,
Animation = function(context)
local suffix, angle = GetDieAnimSuffix(context, context.angle)
return "civ_DeathBlow" .. suffix, true, context.move_pos2, angle
end,
},
{
Name = "Death_NextSlab",
Condition = function(context)
-- the head is in the 2th tile
if context.move_slabs < 2 then
return false
end
if (context.hit_descr.impact_force or 0) < 1 then
return false
end
return true
end,
Animation = function(context)
local suffix, angle = GetDieAnimSuffix(context, context.angle)
local anim = "civ_Death" .. suffix
local pos = context.move_pos1
if not GetPassSlab(pos) then
pos = point(
Clamp(pos:x(), context.move_pos1:x() - const.SlabSizeX / 2, context.move_pos1:x() + const.SlabSizeX / 2 - 1),
Clamp(pos:y(), context.move_pos1:y() - const.SlabSizeY / 2, context.move_pos1:y() + const.SlabSizeY / 2 - 1),
context.move_pos1:z())
end
return "civ_Death" .. suffix, true, pos, angle
end,
},
{
Name = "Death_OnPlace",
Condition = function(context)
return true
end,
Animation = function(context)
local suffix, angle = GetDieAnimSuffix(context, context.angle)
return "civ_DeathOnSpot" .. suffix, true, context.pos, angle
end,
},
}
function GetConditionGraphAnim(context)
if not context.hit_descr then
context.hit_descr = empty_table
end
if context.hit_descr.death_blow then
local simple = table.find_value(s_ConditionsGraph, "Name", "Death_OverNextSlab")
local anim, variations, pos, angle, param = simple.Animation(context)
if anim and IsValidAnim(context.unit, anim) then
return anim, variations, pos, angle, param
end
end
if context.unit.ImportantNPC then
local simple = table.find_value(s_ConditionsGraph, "Name", "Death_OnPlace")
local anim, variations, pos, angle, param = simple.Animation(context)
if anim and IsValidAnim(context.unit, anim) then
return anim, variations, pos, angle, param
end
end
for _, node in ipairs(s_ConditionsGraph) do
if string.match(node.Name, context.pattern) and (context.skip_condition or node.Condition(context)) then
local anim, variations, pos, angle, param = node.Animation(context)
if anim and IsValidAnim(context.unit, anim) then
return anim, variations, pos, angle, param
end
end
end
end
function GetDeathBaseAnim(unit, context)
local hit_descr = context and context.hit_descr
local variations
if unit.species == "Hyena" and hit_descr then
if (hit_descr.death_explosion or (hit_descr.prev_hit_points or Max(0, unit.HitPoints)) - (hit_descr.raw_damage or 0) <= -20) then
variations = { "death2", "death3" }
end
end
if variations then
return variations[1], variations
end
if unit.species ~= "Human" or unit.stance == "Prone" then
local base_anim = unit:TryGetActionAnim("Death", unit.stance)
return base_anim, true
end
context = context or {}
context.unit = context.unit or unit
if not context.pos then
context.pos = context.hit_descr and context.hit_descr.die_pos
if not context.pos then
local x, y, z = FindFallDownPos(unit)
context.pos = x and point(x, y, z) or GetPassSlab(unit) or unit:GetPos()
end
end
if not context.angle then
if context.target_pos then
context.angle = CalcOrientation(unit, context.target_pos)
elseif context.attacker then
context.angle = CalcOrientation(context.attacker, unit)
else
context.angle = unit:GetOrientationAngle() + 180 * 60
end
end
local move_slabs = 0
local move_pos = context.pos
while move_slabs < 4 do
local next_pos = GetPassSlab(RotateRadius((move_slabs + 1) * const.SlabSizeX, context.angle, context.pos))
if not next_pos or not IsPassSlabStep(move_pos, next_pos, const.TunnelTypeWalk) then
break
end
move_slabs = move_slabs + 1
move_pos = next_pos
context["move_pos" .. move_slabs] = next_pos
end
context.move_slabs = move_slabs
if context.close_shot_dist == nil and context.attacker then
if not IsKindOf(context.hit_descr.weapon, "MeleeWeapon") then
if IsCloser(unit, context.attacker, 5 * const.SlabSizeX) then
context.close_shot_dist = true
end
end
end
if not context.pattern then
context.pattern = "^Death_"
end
return GetConditionGraphAnim(context)
end
function GetRandomDeathAnim(unit, context)
local base_anim, variations, pos, angle, param = GetDeathBaseAnim(unit, context)
local anim
if type(variations) == "table" then
anim = variations[1 + (#variations > 1 and unit:Random(#variations) or 0)]
elseif base_anim and variations ~= false then
anim = unit:GetNearbyUniqueRandomAnim(base_anim)
end
if not anim then
anim = base_anim or "death"
end
return anim, pos, angle, param
end
function TestDeathAnim(unit, pos, angle, context, variant)
unit:SetPos(pos or context.pos or GetPassSlab(unit) or unit:GetPos())
unit:SetAxis(axis_z)
unit:SetAngle(angle or unit:GetAngle())
unit:SetState(unit:GetIdleBaseAnim("Standing"), 0, 0)
local base_anim, variations, pos, angle, param = GetDeathBaseAnim(unit, context)
if not base_anim then
print("No death anim")
return
end
if not IsValidAnim(unit, base_anim) then
printf("Invalid death animation: %s", base_anim)
return
end
local anim
if type(variations) == "table" then
anim = variations[variant]
elseif variant and variant > 1 then
anim = base_anim .. variant
else
anim = base_anim
end
if not IsValidAnim(unit, anim) then
printf("Invalid death animation: %s", anim)
return
end
printf("Death animation: %s, angle = %d", anim, angle/60)
unit:SetCommand("PlayDying", false, false, anim, pos, angle, param)
end