Files
WickedEngine/scripts/fighting_game.lua
T
2019-04-20 18:10:53 +02:00

1112 lines
41 KiB
Lua

-- Lua Fighting game sample script
--
-- README:
--
-- Structure of this script:
--
-- **) Character "class" - holds all character specific information, like hitboxes, moves, state machine, and Update(), Input() functions
-- ***) ResolveCharacters() function - updates the two players and checks for collision, moves the camera, etc.
-- ****) Main loop process - initialize script and do call update() in an infinite loop
--
--
-- The script is programmable using common fighting game "numpad notations" (read this if you are unfamiliar: http://www.dustloop.com/wiki/index.php/Notation )
-- There are four action buttons: A, B, C, D
-- So for example a forward motion combined with action D would look like this in code: "6D"
-- A D action without motion (neutral D) would be: "5D"
-- A quarter circle forward + A would be "236A"
-- "Shoryuken" + A command would be: "623A"
-- For a full circle motion, the input would be: "23698741"
-- But because that full circle motion is difficult to execute properly, we can make it easier by accpeting similar inputs, like:
-- "2684" or "2369874"...
-- The require_input("inputstring") facility will help detect instant input execution
-- The require_input_window("inputstring", allowed_latency_window) facility can detect inputs that are executed over multiple frames
-- Neutral motion is "5", that is not necessary to put into input strings in most cases, but it can help, for example: double tap right button would need a neutral in between the two presses, like this: 656
local scene = GetScene()
-- **The character "class" is a wrapper function that returns a local internal table called "self"
local function Character(face, shirt_color)
local self = {
model = INVALID_ENTITY,
effect_dust = INVALID_ENTITY,
face = 1, -- face direction (X)
request_face = 1,
position = Vector(),
velocity = Vector(),
force = Vector(),
frame = 0,
input_buffer = {},
clipbox = AABB(),
hurtboxes = {},
hitboxes = {},
hitconfirm = false,
hurt = false,
-- Common requirement conditions for state transitions:
require_input_window = function(self, inputString, window) -- player input notation with some tolerance to input execution window (in frames) (help: see readme on top of this file)
-- reduce remaining input with non-expired commands:
for i,element in ipairs(self.input_buffer) do
if(element.age <= window and element.command == string.sub(inputString, 0, string.len(element.command))) then
inputString = string.sub(inputString, string.len(element.command) + 1)
if(inputString == "") then
return true
end
end
end
return false -- match failure
end,
require_input = function(self, inputString) -- player input notation (immediate) (help: see readme on top of this file)
return self:require_input_window(inputString, 0)
end,
require_frame = function(self, frame) -- specific frame
return self.frame == frame
end,
require_window = function(self, frameStart, frameEnd) -- frame window range
return self.frame >= frameStart and self.frame <= frameEnd
end,
require_animationfinish = function(self) -- animation is finished
return scene.Component_GetAnimation(self.states[self.state].anim).IsEnded()
end,
require_hitconfirm = function(self) -- true if this player successfully hit the other
return self.hitconfirm
end,
require_hurt = function(self) -- true if this player was hit by the other
return self.hurt
end,
-- Common motion helpers:
require_motion_qcf = function(self, button)
local window = 20
return
self:require_input_window("236" .. button, window) or
self:require_input_window("26" .. button, window)
end,
require_motion_shoryuken = function(self, button)
local window = 20
return
self:require_input_window("623" .. button, window) or
self:require_input_window("626" .. button, window)
end,
-- List all possible states:
states = {
Idle = {
anim_name = "Idle",
anim = INVALID_ENTITY,
clipbox = AABB(Vector(-1), Vector(1, 5)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 5.5)),
},
Walk_Backward = {
anim_name = "Back",
anim = INVALID_ENTITY,
clipbox = AABB(Vector(-1), Vector(1, 5)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 5.5)),
update = function(self)
self.force = vector.Add(self.force, Vector(-0.025 * self.face, 0))
end,
},
Walk_Forward = {
anim_name = "Forward",
anim = INVALID_ENTITY,
clipbox = AABB(Vector(-1), Vector(1, 5)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 5.5)),
update = function(self)
self.force = vector.Add(self.force, Vector(0.025 * self.face, 0))
end,
},
Dash_Backward = {
anim_name = "BDash",
anim = INVALID_ENTITY,
looped = false,
clipbox = AABB(Vector(-1), Vector(1, 5)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 5.5)),
update = function(self)
if(self:require_window(0,2)) then
self.force = vector.Add(self.force, Vector(-0.07 * self.face, 0.1))
end
end,
},
RunStart = {
anim_name = "RunStart",
anim = INVALID_ENTITY,
clipbox = AABB(Vector(-0.5), Vector(2, 5)),
hurtbox = AABB(Vector(-0.7), Vector(2.2, 5.5)),
},
Run = {
anim_name = "Run",
anim = INVALID_ENTITY,
clipbox = AABB(Vector(-0.5), Vector(2, 5)),
hurtbox = AABB(Vector(-0.7), Vector(2.2, 5.5)),
update = function(self)
self.force = vector.Add(self.force, Vector(0.08 * self.face, 0))
end,
},
RunEnd = {
anim_name = "RunEnd",
anim = INVALID_ENTITY,
clipbox = AABB(Vector(-0.5), Vector(2, 5)),
hurtbox = AABB(Vector(-0.7), Vector(2.2, 5.5)),
},
Jump = {
anim_name = "Jump",
anim = INVALID_ENTITY,
looped = false,
clipbox = AABB(Vector(-1), Vector(1, 5)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 5.5)),
update = function(self)
if(self.frame == 0) then
self.force = vector.Add(self.force, Vector(0, 0.8))
end
end,
},
JumpBack = {
anim_name = "Jump",
anim = INVALID_ENTITY,
looped = false,
clipbox = AABB(Vector(-1), Vector(1, 5)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 5.5)),
update = function(self)
if(self.frame == 0) then
self.force = vector.Add(self.force, Vector(-0.2 * self.face, 0.8))
end
end,
},
JumpForward = {
anim_name = "Jump",
anim = INVALID_ENTITY,
looped = false,
clipbox = AABB(Vector(-1), Vector(1, 5)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 5.5)),
update = function(self)
if(self.frame == 0) then
self.force = vector.Add(self.force, Vector(0.2 * self.face, 0.8))
end
end,
},
FallStart = {
anim_name = "FallStart",
anim = INVALID_ENTITY,
looped = false,
clipbox = AABB(Vector(-1), Vector(1, 5)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 5.5)),
},
Fall = {
anim_name = "Fall",
anim = INVALID_ENTITY,
clipbox = AABB(Vector(-1), Vector(1, 5)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 5.5)),
},
FallEnd = {
anim_name = "FallEnd",
anim = INVALID_ENTITY,
looped = false,
clipbox = AABB(Vector(-1), Vector(1, 5)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 5.5)),
update = function(self)
if(self:require_frame(2)) then
local emitter_component = scene.Component_GetEmitter(self.effect_dust)
emitter_component.Burst(10)
local transform_component = scene.Component_GetTransform(self.effect_dust)
transform_component.ClearTransform()
transform_component.Translate(self.position)
end
end,
},
CrouchStart = {
anim_name = "CrouchStart",
anim = INVALID_ENTITY,
looped = false,
clipbox = AABB(Vector(-1), Vector(1, 3)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 3.5)),
},
Crouch = {
anim_name = "Crouch",
anim = INVALID_ENTITY,
clipbox = AABB(Vector(-1), Vector(1, 3)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 3.5)),
},
CrouchEnd = {
anim_name = "CrouchEnd",
anim = INVALID_ENTITY,
looped = false,
clipbox = AABB(Vector(-1), Vector(1, 5)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 5.5)),
},
Turn = {
anim_name = "Turn",
anim = INVALID_ENTITY,
looped = false,
clipbox = AABB(Vector(-1), Vector(1, 5)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 5.5)),
update = function(self)
if(self.frame == 0) then
self.face = self.request_face
end
end,
},
LightPunch = {
anim_name = "LightPunch",
anim = INVALID_ENTITY,
looped = false,
clipbox = AABB(Vector(-1), Vector(1, 5)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 5.5)),
update = function(self)
if(self:require_window(3,6)) then
table.insert(self.hitboxes, AABB(Vector(0.5,2), Vector(3,5)) )
end
end,
},
ForwardLightPunch = {
anim_name = "FLightPunch",
anim = INVALID_ENTITY,
looped = false,
clipbox = AABB(Vector(-1), Vector(1, 5)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 5.5)),
update = function(self)
if(self:require_window(12,14)) then
table.insert(self.hitboxes, AABB(Vector(0.5,2), Vector(3.5,6)) )
end
end,
},
HeavyPunch = {
anim_name = "HeavyPunch",
anim = INVALID_ENTITY,
looped = false,
clipbox = AABB(Vector(-1), Vector(1, 5)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 5.5)),
update = function(self)
if(self:require_window(3,6)) then
table.insert(self.hitboxes, AABB(Vector(0.5,2), Vector(3.5,5)) )
end
end,
},
LowPunch = {
anim_name = "LowPunch",
anim = INVALID_ENTITY,
looped = false,
clipbox = AABB(Vector(-1), Vector(1, 3)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 3.5)),
update = function(self)
if(self:require_window(3,6)) then
table.insert(self.hitboxes, AABB(Vector(0.5,0), Vector(2.8,3)) )
end
end,
},
LightKick = {
anim_name = "LightKick",
anim = INVALID_ENTITY,
looped = false,
clipbox = AABB(Vector(-1), Vector(1, 5)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 5.5)),
update = function(self)
if(self:require_window(6,8)) then
table.insert(self.hitboxes, AABB(Vector(0,0), Vector(3,3)) )
end
end,
},
HeavyKick = {
anim_name = "HeavyKick",
anim = INVALID_ENTITY,
looped = false,
clipbox = AABB(Vector(-1), Vector(1, 5)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 5.5)),
update = function(self)
if(self:require_window(8,13)) then
table.insert(self.hitboxes, AABB(Vector(0,0), Vector(4,3)) )
end
end,
},
LowKick = {
anim_name = "LowKick",
anim = INVALID_ENTITY,
looped = false,
clipbox = AABB(Vector(-1), Vector(1, 3)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 3.5)),
update = function(self)
if(self:require_window(3,6)) then
table.insert(self.hitboxes, AABB(Vector(0.5,0), Vector(3,3)) )
end
end,
},
Uppercut = {
anim_name = "Uppercut",
anim = INVALID_ENTITY,
looped = false,
clipbox = AABB(Vector(-1), Vector(1, 5)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 5.5)),
update = function(self)
if(self:require_window(3,14)) then
table.insert(self.hitboxes, AABB(Vector(0,3), Vector(2.3,7)) )
end
end,
},
SpearJaunt = {
anim_name = "SpearJaunt",
anim = INVALID_ENTITY,
looped = false,
clipbox = AABB(Vector(-1.5), Vector(1.5, 5)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 5.5)),
update = function(self)
if(self:require_frame(16)) then
self.force = vector.Add(self.force, Vector(1.3 * self.face))
end
if(self:require_window(17,40)) then
table.insert(self.hitboxes, AABB(Vector(0,1), Vector(4.5,5)) )
end
end,
},
Shoryuken = {
anim_name = "Shoryuken",
anim = INVALID_ENTITY,
looped = false,
clipbox = AABB(Vector(-1), Vector(1, 5)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 5.5)),
update = function(self)
if(self:require_frame(0)) then
self.force = vector.Add(self.force, Vector(0.3 * self.face, 0.9))
end
if(self:require_window(2,30)) then
table.insert(self.hitboxes, AABB(Vector(0,3), Vector(2.3,7)) )
end
end,
},
StaggerStart = {
anim_name = "StaggerStart",
anim = INVALID_ENTITY,
looped = false,
clipbox = AABB(Vector(-1), Vector(1, 5)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 5.5)),
},
Stagger = {
anim_name = "Stagger",
anim = INVALID_ENTITY,
looped = false,
clipbox = AABB(Vector(-1), Vector(1, 5)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 5.5)),
},
StaggerEnd = {
anim_name = "StaggerEnd",
anim = INVALID_ENTITY,
looped = false,
clipbox = AABB(Vector(-1), Vector(1, 5)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 5.5)),
},
StaggerCrouchStart = {
anim_name = "StaggerCrouchStart",
anim = INVALID_ENTITY,
looped = false,
clipbox = AABB(Vector(-1), Vector(1, 5)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 5.5)),
},
StaggerCrouch = {
anim_name = "StaggerCrouch",
anim = INVALID_ENTITY,
looped = false,
clipbox = AABB(Vector(-1), Vector(1, 5)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 5.5)),
},
StaggerCrouchEnd = {
anim_name = "StaggerCrouchEnd",
anim = INVALID_ENTITY,
looped = false,
clipbox = AABB(Vector(-1), Vector(1, 5)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 5.5)),
},
StaggerAirStart = {
anim_name = "StaggerAirStart",
anim = INVALID_ENTITY,
looped = false,
clipbox = AABB(Vector(-1), Vector(1, 5)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 5.5)),
update = function(self)
self.velocity = Vector()
end,
},
StaggerAir = {
anim_name = "StaggerAir",
anim = INVALID_ENTITY,
looped = false,
clipbox = AABB(Vector(-1), Vector(1, 5)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 5.5)),
update = function(self)
self.velocity = Vector()
self.force = vector.Add(self.force, Vector(-0.3 * self.face))
end,
},
StaggerAirEnd = {
anim_name = "StaggerAirEnd",
anim = INVALID_ENTITY,
looped = false,
clipbox = AABB(Vector(-1), Vector(1, 5)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 5.5)),
},
Downed = {
anim_name = "Downed",
anim = INVALID_ENTITY,
clipbox = AABB(Vector(-1), Vector(1, 5)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 5.5)),
},
Getup = {
anim_name = "Getup",
anim = INVALID_ENTITY,
looped = false,
clipbox = AABB(Vector(-1), Vector(1, 5)),
hurtbox = AABB(Vector(-1.2), Vector(1.2, 5.5)),
},
},
-- State machine describes all possible state transitions (item order is priority high->low):
-- StateFrom = {
-- { "StateTo1", condition = function(self) return [requirements that should be met] end },
-- { "StateTo2", condition = function(self) return [requirements that should be met] end },
-- }
statemachine = {
Idle = {
{ "StaggerStart", condition = function(self) return self:require_hurt() end, },
{ "Shoryuken", condition = function(self) return self:require_motion_shoryuken("D") end, },
{ "SpearJaunt", condition = function(self) return self:require_motion_qcf("D") end, },
{ "Turn", condition = function(self) return self.request_face ~= self.face and self:require_input("5") end, },
{ "Walk_Forward", condition = function(self) return self:require_input("6") end, },
{ "Walk_Backward", condition = function(self) return self:require_input("4") end, },
{ "Jump", condition = function(self) return self:require_input("8") end, },
{ "JumpBack", condition = function(self) return self:require_input("7") end, },
{ "JumpForward", condition = function(self) return self:require_input("9") end, },
{ "CrouchStart", condition = function(self) return self:require_input("1") or self:require_input("2") or self:require_input("3") end, },
{ "LightPunch", condition = function(self) return self:require_input("5A") end, },
{ "HeavyPunch", condition = function(self) return self:require_input("5B") end, },
{ "LightKick", condition = function(self) return self:require_input("5C") end, },
},
Walk_Backward = {
{ "StaggerStart", condition = function(self) return self:require_hurt() end, },
{ "Shoryuken", condition = function(self) return self:require_motion_shoryuken("D") end, },
{ "CrouchStart", condition = function(self) return self:require_input("1") or self:require_input("2") or self:require_input("3") end, },
{ "Walk_Forward", condition = function(self) return self:require_input("6") end, },
{ "Dash_Backward", condition = function(self) return self:require_input_window("454", 7) end, },
{ "JumpBack", condition = function(self) return self:require_input("7") end, },
{ "Idle", condition = function(self) return self:require_input("5") end, },
{ "LightPunch", condition = function(self) return self:require_input("5A") end, },
{ "HeavyPunch", condition = function(self) return self:require_input("5B") end, },
{ "LightKick", condition = function(self) return self:require_input("5C") end, },
{ "ForwardLightPunch", condition = function(self) return self:require_input("6A") end, },
{ "HeavyKick", condition = function(self) return self:require_input("6C") end, },
},
Walk_Forward = {
{ "StaggerStart", condition = function(self) return self:require_hurt() end, },
{ "Shoryuken", condition = function(self) return self:require_motion_shoryuken("D") end, },
{ "SpearJaunt", condition = function(self) return self:require_motion_qcf("D") end, },
{ "CrouchStart", condition = function(self) return self:require_input("1") or self:require_input("2") or self:require_input("3") end, },
{ "Walk_Backward", condition = function(self) return self:require_input("4") end, },
{ "RunStart", condition = function(self) return self:require_input_window("656", 7) end, },
{ "JumpForward", condition = function(self) return self:require_input("9") end, },
{ "Idle", condition = function(self) return self:require_input("5") end, },
{ "LightPunch", condition = function(self) return self:require_input("5A") end, },
{ "HeavyPunch", condition = function(self) return self:require_input("5B") end, },
{ "LightKick", condition = function(self) return self:require_input("5C") end, },
{ "ForwardLightPunch", condition = function(self) return self:require_input("6A") end, },
{ "HeavyKick", condition = function(self) return self:require_input("6C") end, },
},
Dash_Backward = {
{ "StaggerStart", condition = function(self) return self:require_hurt() end, },
{ "Idle", condition = function(self) return self:require_animationfinish() end, },
},
RunStart = {
{ "StaggerStart", condition = function(self) return self:require_hurt() end, },
{ "Run", condition = function(self) return self:require_animationfinish() end, },
},
Run = {
{ "StaggerStart", condition = function(self) return self:require_hurt() end, },
{ "RunEnd", condition = function(self) return self:require_input("5") end, },
{ "Jump", condition = function(self) return self:require_input("8") end, },
{ "JumpBack", condition = function(self) return self:require_input("7") end, },
{ "JumpForward", condition = function(self) return self:require_input("9") end, },
},
RunEnd = {
{ "StaggerStart", condition = function(self) return self:require_hurt() end, },
{ "Idle", condition = function(self) return self:require_animationfinish() end, },
},
Jump = {
{ "StaggerAirStart", condition = function(self) return self:require_hurt() and self.position.GetY() > 0 end, },
{ "StaggerStart", condition = function(self) return self:require_hurt() end, },
{ "FallStart", condition = function(self) return self.velocity.GetY() <= 0 end, },
},
JumpForward = {
{ "StaggerAirStart", condition = function(self) return self:require_hurt() and self.position.GetY() > 0 end, },
{ "StaggerStart", condition = function(self) return self:require_hurt() end, },
{ "FallStart", condition = function(self) return self.velocity.GetY() <= 0 end, },
},
JumpBack = {
{ "StaggerAirStart", condition = function(self) return self:require_hurt() and self.position.GetY() > 0 end, },
{ "StaggerStart", condition = function(self) return self:require_hurt() end, },
{ "FallStart", condition = function(self) return self.velocity.GetY() <= 0 end, },
},
FallStart = {
{ "StaggerAirStart", condition = function(self) return self:require_hurt() and self.position.GetY() > 0 end, },
{ "StaggerStart", condition = function(self) return self:require_hurt() end, },
{ "FallEnd", condition = function(self) return self.position.GetY() <= 0.5 end, },
{ "Fall", condition = function(self) return self:require_animationfinish() end, },
},
Fall = {
{ "StaggerAirStart", condition = function(self) return self:require_hurt() and self.position.GetY() > 0 end, },
{ "StaggerStart", condition = function(self) return self:require_hurt() end, },
{ "FallEnd", condition = function(self) return self.position.GetY() <= 0.5 end, },
},
FallEnd = {
{ "StaggerAirStart", condition = function(self) return self:require_hurt() and self.position.GetY() > 0 end, },
{ "StaggerStart", condition = function(self) return self:require_hurt() end, },
{ "Idle", condition = function(self) return self.position.GetY() <= 0 and self:require_animationfinish() end, },
},
CrouchStart = {
{ "StaggerCrouchStart", condition = function(self) return self:require_hurt() end, },
{ "Idle", condition = function(self) return self:require_input("5") end, },
{ "Crouch", condition = function(self) return (self:require_input("1") or self:require_input("2") or self:require_input("3")) and self:require_animationfinish() end, },
},
Crouch = {
{ "StaggerCrouchStart", condition = function(self) return self:require_hurt() end, },
{ "CrouchEnd", condition = function(self) return self:require_input("5") or self:require_input("4") or self:require_input("6") or self:require_input("7") or self:require_input("8") or self:require_input("9") end, },
{ "LowPunch", condition = function(self) return self:require_input("2A") or self:require_input("1A") or self:require_input("3A") end, },
{ "LowKick", condition = function(self) return self:require_input("2C") or self:require_input("1C") or self:require_input("3C") end, },
{ "Uppercut", condition = function(self) return self:require_input("2B") or self:require_input("1B") or self:require_input("3B") end, },
},
CrouchEnd = {
{ "StaggerStart", condition = function(self) return self:require_hurt() end, },
{ "Idle", condition = function(self) return self:require_animationfinish() end, },
},
Turn = {
{ "StaggerStart", condition = function(self) return self:require_hurt() end, },
{ "Idle", condition = function(self) return self:require_animationfinish() end, },
},
LightPunch = {
{ "StaggerStart", condition = function(self) return self:require_hurt() end, },
{ "Idle", condition = function(self) return self:require_animationfinish() end, },
},
ForwardLightPunch = {
{ "StaggerStart", condition = function(self) return self:require_hurt() end, },
{ "Idle", condition = function(self) return self:require_animationfinish() end, },
},
HeavyPunch = {
{ "StaggerStart", condition = function(self) return self:require_hurt() end, },
{ "Idle", condition = function(self) return self:require_animationfinish() end, },
},
LowPunch = {
{ "StaggerStart", condition = function(self) return self:require_hurt() end, },
{ "Crouch", condition = function(self) return self:require_animationfinish() end, },
},
LightKick = {
{ "StaggerStart", condition = function(self) return self:require_hurt() end, },
{ "Idle", condition = function(self) return self:require_animationfinish() end, },
},
HeavyKick = {
{ "StaggerStart", condition = function(self) return self:require_hurt() end, },
{ "Idle", condition = function(self) return self:require_animationfinish() end, },
},
LowKick = {
{ "StaggerStart", condition = function(self) return self:require_hurt() end, },
{ "Crouch", condition = function(self) return self:require_animationfinish() end, },
},
Uppercut = {
{ "StaggerStart", condition = function(self) return self:require_hurt() end, },
{ "Idle", condition = function(self) return self:require_animationfinish() end, },
},
SpearJaunt = {
{ "StaggerStart", condition = function(self) return self:require_hurt() end, },
{ "Idle", condition = function(self) return self:require_animationfinish() end, },
},
Shoryuken = {
{ "StaggerStart", condition = function(self) return self:require_hurt() end, },
{ "FallStart", condition = function(self) return self:require_animationfinish() end, },
},
StaggerStart = {
{ "StaggerStart", condition = function(self) return self:require_hurt() end, },
{ "Stagger", condition = function(self) return self:require_animationfinish() end, },
},
Stagger = {
{ "StaggerStart", condition = function(self) return self:require_hurt() end, },
{ "StaggerEnd", condition = function(self) return not self:require_hurt() end, },
},
StaggerEnd = {
{ "StaggerStart", condition = function(self) return self:require_hurt() end, },
{ "Idle", condition = function(self) return self:require_animationfinish() end, },
},
StaggerCrouchStart = {
{ "StaggerCrouchStart", condition = function(self) return self:require_hurt() end, },
{ "StaggerCrouch", condition = function(self) return self:require_animationfinish() end, },
},
StaggerCrouch = {
{ "StaggerCrouchStart", condition = function(self) return self:require_hurt() end, },
{ "StaggerCrouchEnd", condition = function(self) return not self:require_hurt() end, },
},
StaggerCrouchEnd = {
{ "StaggerCrouchStart", condition = function(self) return self:require_hurt() end, },
{ "Crouch", condition = function(self) return self:require_animationfinish() end, },
},
StaggerAirStart = {
{ "StaggerAirStart", condition = function(self) return self:require_hurt() end, },
{ "StaggerAir", condition = function(self) return self:require_animationfinish() end, },
},
StaggerAir = {
{ "StaggerAirStart", condition = function(self) return self:require_hurt() end, },
{ "StaggerAirEnd", condition = function(self) return not self:require_hurt() end, },
},
StaggerAirEnd = {
{ "StaggerAirStart", condition = function(self) return self:require_hurt() end, },
{ "Downed", condition = function(self) return self:require_animationfinish() and self.position.GetY() < 0.2 end, },
},
Downed = {
{ "Getup", condition = function(self) return self:require_input("A") or self:require_input("B") or self:require_input("C") or self.frame > 60 end, },
},
Getup = {
{ "Idle", condition = function(self) return self:require_animationfinish() end, },
},
},
state = "Idle", -- starting state
-- Ends the current state:
EndState = function(self)
scene.Component_GetAnimation(self.states[self.state].anim).Stop()
end,
-- Starts a new state:
StartState = function(self, dst_state)
scene.Component_GetAnimation(self.states[dst_state].anim).Play()
self.frame = 0
self.state = dst_state
end,
-- Parse state machine at current state and perform transition if applicable:
StepStateMachine = function(self)
local transition_candidates = self.statemachine[self.state]
if(transition_candidates ~= nil) then
for i,dst in pairs(transition_candidates) do
-- check transition requirement conditions:
local requirements_met = true
if(dst.condition ~= nil) then
requirements_met = dst.condition(self)
end
if(requirements_met) then
-- transition to new state when all requirements are met:
self:EndState()
self:StartState(dst[1])
return
end
end
end
end,
-- Execute the currently active state:
ExecuteCurrentState = function(self)
self.clipbox = AABB()
self.hurtboxes = {}
self.hitboxes = {}
local current_state = self.states[self.state]
if(current_state ~= nil) then
if(current_state.update ~= nil) then
current_state.update(self)
end
if(current_state.clipbox ~= nil) then
self.clipbox = current_state.clipbox
end
if(current_state.hurtbox ~= nil) then
table.insert(self.hurtboxes, current_state.hurtbox)
end
end
end,
Create = function(self, face, shirt_color)
-- Load the model into a custom scene:
-- We use a custom scene because if two models are loaded into the global scene, they will have name collisions
-- and thus we couldn't properly query entities by name
local model_scene = Scene()
self.model = LoadModel(model_scene, "../models/havoc/havoc.wiscene")
-- Place model according to starting facing direction:
self.face = face
self.request_face = face
self.position = Vector(self.face * -4)
-- Set shirt color todifferentiate between characters:
local shirt_material_entity = model_scene.Entity_FindByName("material_shirt")
model_scene.Component_GetMaterial(shirt_material_entity).SetBaseColor(shirt_color)
-- Initialize states:
for i,state in pairs(self.states) do
state.anim = model_scene.Entity_FindByName(state.anim_name)
if(state.looped ~= nil) then
model_scene.Component_GetAnimation(state.anim).SetLooped(state.looped)
end
end
-- Move the custom scene into the global scene:
scene.Merge(model_scene)
-- Load effects:
local effect_scene = Scene()
local model_dust = LoadModel(effect_scene, "../models/emitter_dust.wiscene")
self.effect_dust = effect_scene.Entity_FindByName("dust") -- query the emitter entity by name
local emitter_component = effect_scene.Component_GetEmitter(self.effect_dust)
emitter_component.SetEmitCount(0) -- don't emit continuously
scene.Merge(effect_scene)
self:StartState(self.state)
end,
ai_state = "Idle",
AI = function(self)
-- todo some better AI bot behaviour
if(self.ai_state == "Jump") then
table.insert(self.input_buffer, {age = 0, command = "8"})
elseif(self.ai_state == "Crouch") then
table.insert(self.input_buffer, {age = 0, command = "2"})
else
table.insert(self.input_buffer, {age = 0, command = "5"})
end
end,
Input = function(self)
-- read input (todo gamepad):
local left = input.Down(string.byte('A'))
local right = input.Down(string.byte('D'))
local up = input.Down(string.byte('W'))
local down = input.Down(string.byte('S'))
local A = input.Press(VK_RIGHT)
local B = input.Press(VK_UP)
local C = input.Press(VK_LEFT)
local D = input.Press(VK_DOWN)
-- swap left and right if facing is opposite side:
if(self.face < 0) then
local tmp = right
right = left
left = tmp
end
if(up and left) then
table.insert(self.input_buffer, {age = 0, command = "7"})
elseif(up and right) then
table.insert(self.input_buffer, {age = 0, command = "9"})
elseif(up) then
table.insert(self.input_buffer, {age = 0, command = "8"})
elseif(down and left) then
table.insert(self.input_buffer, {age = 0, command = "1"})
elseif(down and right) then
table.insert(self.input_buffer, {age = 0, command = "3"})
elseif(down) then
table.insert(self.input_buffer, {age = 0, command = "2"})
elseif(left) then
table.insert(self.input_buffer, {age = 0, command = "4"})
elseif(right) then
table.insert(self.input_buffer, {age = 0, command = "6"})
else
table.insert(self.input_buffer, {age = 0, command = "5"})
end
if(A) then
table.insert(self.input_buffer, {age = 0, command = "A"})
end
if(B) then
table.insert(self.input_buffer, {age = 0, command = "B"})
end
if(C) then
table.insert(self.input_buffer, {age = 0, command = "C"})
end
if(D) then
table.insert(self.input_buffer, {age = 0, command = "D"})
end
end,
Update = function(self)
self.frame = self.frame + 1
self:StepStateMachine()
self:ExecuteCurrentState()
-- Manage input buffer:
for i,element in pairs(self.input_buffer) do -- every input gets older by one frame
element.age = element.age + 1
end
if(#self.input_buffer > 60) then -- only keep the last 60 inputs
table.remove(self.input_buffer, 1)
end
-- force from gravity:
self.force = vector.Add(self.force, Vector(0,-0.04,0))
-- apply force:
self.velocity = vector.Add(self.velocity, self.force)
self.force = Vector()
-- aerial drag:
self.velocity = vector.Multiply(self.velocity, 0.98)
-- apply velocity:
self.position = vector.Add(self.position, self.velocity)
-- check if we are below or on the ground:
if(self.position.GetY() <= 0 and self.velocity.GetY()<=0) then
self.position.SetY(0) -- snap to ground
self.velocity.SetY(0) -- don't fall below ground
self.velocity = vector.Multiply(self.velocity, 0.8) -- ground drag:
end
-- Transform component gets set as absolute coordinates every frame:
local model_transform = scene.Component_GetTransform(self.model)
model_transform.ClearTransform()
model_transform.Translate(self.position)
model_transform.Rotate(Vector(0, 3.1415 * ((self.face - 1) * 0.5)))
model_transform.UpdateTransform()
local model_mat = model_transform.GetMatrix()
self.clipbox = self.clipbox.Transform(model_mat)
for i,hitbox in ipairs(self.hitboxes) do
self.hitboxes[i] = hitbox.Transform(model_mat)
DrawBox(self.hitboxes[i].GetAsBoxMatrix(), Vector(1,0,0,1))
end
for i,hurtbox in ipairs(self.hurtboxes) do
self.hurtboxes[i] = hurtbox.Transform(model_mat)
DrawBox(self.hurtboxes[i].GetAsBoxMatrix(), Vector(0,1,0,1))
end
-- Some debug draw:
DrawPoint(model_transform.GetPosition(), 0.1, Vector(1,0,0,1))
DrawLine(model_transform.GetPosition(),model_transform.GetPosition():Add(self.velocity), Vector(0,1,0,1))
DrawLine(model_transform.GetPosition(),model_transform.GetPosition():Add(Vector(self.face)), Vector(0,0,1,1))
DrawBox(self.clipbox.GetAsBoxMatrix(), Vector(1,1,0,1))
end
}
self:Create(face, shirt_color)
return self
end
-- script camera state:
local camera_position = Vector()
local camera_transform = TransformComponent()
-- ***Interaction between two characters:
local ResolveCharacters = function(player1, player2)
player1:Input()
player2:AI()
player1:Update()
player2:Update()
-- Hit/Hurt:
player1.hitconfirm = false
player1.hurt = false
player2.hitconfirm = false
player2.hurt = false
for i,hitbox in pairs(player1.hitboxes) do
for j,hurtbox in pairs(player2.hurtboxes) do
if(hitbox.Intersects2D(hurtbox)) then
player1.hitconfirm = true
player2.hurt = true
break
end
end
end
for i,hitbox in ipairs(player2.hitboxes) do
for j,hurtbox in ipairs(player1.hurtboxes) do
if(hitbox.Intersects2D(hurtbox)) then
player2.hitconfirm = true
player1.hurt = true
break
end
end
end
-- Clipping:
if(player1.clipbox.Intersects2D(player2.clipbox)) then
local center1 = player1.clipbox.GetCenter().GetX()
local center2 = player2.clipbox.GetCenter().GetX()
local extent1 = player1.clipbox.GetHalfExtents().GetX()
local extent2 = player2.clipbox.GetHalfExtents().GetX()
local diff = math.abs(center2 - center1)
local target_diff = math.abs(extent2 + extent1)
local offset = (target_diff - diff) * 0.5
player1.position.SetX(player1.position.GetX() - offset * player1.request_face)
player2.position.SetX(player2.position.GetX() - offset * player2.request_face)
end
-- Facing direction requests:
if(player1.position.GetX() < player2.position.GetX()) then
player1.request_face = 1
player2.request_face = -1
else
player1.request_face = -1
player2.request_face = 1
end
-- Camera:
local CAMERA_HEIGHT = 4 -- camera height from ground
local DEFAULT_CAMERADISTANCE = -9.5 -- the default camera distance when characters are close to each other
local MODIFIED_CAMERADISTANCE = -11.5 -- if the two players are far enough from each other, the camera will zoom out to this distance
local CAMERA_DISTANCE_MODIFIER = 10 -- the required distance between the characters when the camera should zoom out
local XBOUNDS = 20 -- play area horizontal bounds
local CAMERA_SIDE_LENGTH = 11 -- play area inside the camera (character can't move outside camera even if inside the play area)
-- Clamp the players inside the camera:
local camera_side_left = camera_position.GetX() - CAMERA_SIDE_LENGTH
local camera_side_right = camera_position.GetX() + CAMERA_SIDE_LENGTH
player1.position.SetX(math.clamp(player1.position.GetX(), camera_side_left, camera_side_right))
player2.position.SetX(math.clamp(player2.position.GetX(), camera_side_left, camera_side_right))
local camera_position_new = Vector()
local distanceX = math.abs(player1.position.GetX() - player2.position.GetX())
local distanceY = math.abs(player1.position.GetY() - player2.position.GetY())
-- camera height:
if(player1.position.GetY() > 4 or player2.position.GetY() > 4) then
camera_position_new.SetY( math.min(player1.position.GetY(), player2.position.GetY()) + distanceY )
else
camera_position_new.SetY(CAMERA_HEIGHT)
end
-- camera distance:
if(distanceX > CAMERA_DISTANCE_MODIFIER) then
camera_position_new.SetZ(MODIFIED_CAMERADISTANCE)
else
camera_position_new.SetZ(DEFAULT_CAMERADISTANCE)
end
-- camera horizontal position:
local centerX = math.clamp((player1.position.GetX() + player2.position.GetX()) * 0.5, -XBOUNDS, XBOUNDS)
camera_position_new.SetX(centerX)
-- smooth camera:
camera_position = vector.Lerp(camera_position, camera_position_new, 0.1)
-- finally update the global camera with current values:
camera_transform.ClearTransform()
camera_transform.Translate(camera_position)
camera_transform.UpdateTransform()
GetCamera().TransformCamera(camera_transform)
end
-- ****Main loop:
runProcess(function()
ClearWorld() -- clears global scene and renderer
SetProfilerEnabled(false) -- have a bit more screen space
-- Fighting game needs stable frame rate and deterministic controls at all times. We will also refer to frames in this script instead of time units.
-- We lock the framerate to 60 FPS, so if frame rate goes below, game will play slower
--
-- There is also the possibility to implement game logic in fixed_update() instead, but that is not common for fighting games
main.SetTargetFrameRate(60)
main.SetFrameRateLock(true)
-- We will override the render path so we can invoke the script from Editor and controls won't collide with editor scripts
-- Also save the active component that we can restore when ESCAPE is pressed
local prevPath = main.GetActivePath()
local path = RenderPath3D_TiledForward()
main.SetActivePath(path)
local help_text = ""
help_text = help_text .. "This script is showcasing how to write a simple fighting game."
help_text = help_text .. "\nControls:\n#####################\nESCAPE key: quit\nR: reload script"
help_text = help_text .. "\nWASD: move"
help_text = help_text .. "\nRight: action A"
help_text = help_text .. "\nUp: action B"
help_text = help_text .. "\nLeft: action C"
help_text = help_text .. "\nDown: action D"
help_text = help_text .. "\nJ: player2 will always jump"
help_text = help_text .. "\nC: player2 will always crouch"
help_text = help_text .. "\nI: player2 will be idle"
help_text = help_text .. "\n\nMovelist:"
help_text = help_text .. "\n\t A : Light Punch"
help_text = help_text .. "\n\t B : Heavy Punch"
help_text = help_text .. "\n\t C : Light Kick"
help_text = help_text .. "\n\t 6A : Forward Light Punch"
help_text = help_text .. "\n\t 6C : Heavy Kick"
help_text = help_text .. "\n\t 2A : Low Punch"
help_text = help_text .. "\n\t 2B : Uppercut"
help_text = help_text .. "\n\t 2C : Low Kick"
help_text = help_text .. "\n\t 623D: Shoryuken"
help_text = help_text .. "\n\t 236D: Jaunt"
local font = Font(help_text);
font.SetSize(20)
font.SetPos(Vector(10, GetScreenHeight() - 10))
font.SetAlign(WIFALIGN_LEFT, WIFALIGN_BOTTOM)
font.SetColor(0xFFADA3FF)
font.SetShadowColor(Vector(0,0,0,1))
path.AddFont(font)
local info = Font("");
info.SetSize(20)
info.SetPos(Vector(GetScreenWidth() / 2, GetScreenHeight() * 0.9))
info.SetAlign(WIFALIGN_LEFT, WIFALIGN_CENTER)
info.SetShadowColor(Vector(0,0,0,1))
path.AddFont(info)
LoadModel("../models/dojo.wiscene")
-- Create the two player characters. Parameters are facing direction and shirt material color to differentiate between them:
local player1 = Character(1, Vector(1,1,1,1)) -- facing to right, white shirt
local player2 = Character(-1, Vector(1,0,0,1)) -- facing to left, red shirt
while true do
ResolveCharacters(player1, player2)
if(input.Press(string.byte('I'))) then
player2.ai_state = "Idle"
elseif(input.Press(string.byte('J'))) then
player2.ai_state = "Jump"
elseif(input.Press(string.byte('C'))) then
player2.ai_state = "Crouch"
end
local inputString = "input: "
for i,element in ipairs(player1.input_buffer) do
if(element.command ~= "5") then
inputString = inputString .. element.command
end
end
info.SetText(inputString .. "\nstate = " .. player1.state .. "\nframe = " .. player1.frame)
-- Wait for Engine update tick
update()
if(input.Press(VK_ESCAPE)) then
-- restore previous component
-- so if you loaded this script from the editor, you can go back to the editor with ESC
backlog_post("EXIT")
killProcesses()
main.SetActivePath(prevPath)
return
end
if(input.Press(string.byte('R'))) then
-- reload script
backlog_post("RELOAD")
killProcesses()
main.SetActivePath(prevPath)
dofile("fighting_game.lua")
return
end
end
end)