273 lines
7.8 KiB
Lua
273 lines
7.8 KiB
Lua
|
|
||
|
--
|
||
|
-- constants
|
||
|
--
|
||
|
|
||
|
local tilting_speed = 1
|
||
|
local tilting_max = 0.5
|
||
|
local power_max = 20
|
||
|
local power_min = 0.2 -- if negative, the helicopter can actively fly downwards
|
||
|
local wanted_vert_speed = 10
|
||
|
local friction_air_quadratic = 0.01
|
||
|
local friction_air_constant = 0.2
|
||
|
local friction_land_quadratic = 1
|
||
|
local friction_land_constant = 2
|
||
|
local friction_water_quadratic = 0.1
|
||
|
local friction_water_constant = 1
|
||
|
|
||
|
--
|
||
|
-- helpers and co.
|
||
|
--
|
||
|
|
||
|
if not minetest.global_exists("matrix3") then
|
||
|
dofile(minetest.get_modpath("helicopter") .. DIR_DELIM .. "matrix.lua")
|
||
|
end
|
||
|
|
||
|
local creative_exists = minetest.global_exists("creative")
|
||
|
local gravity = tonumber(minetest.settings:get("movement_gravity")) or 9.8
|
||
|
local vector_up = vector.new(0, 1, 0)
|
||
|
local vector_forward = vector.new(0, 0, 1)
|
||
|
|
||
|
local function vector_length_sq(v)
|
||
|
return v.x * v.x + v.y * v.y + v.z * v.z
|
||
|
end
|
||
|
|
||
|
local function check_node_below(obj)
|
||
|
local pos_below = obj:get_pos()
|
||
|
pos_below.y = pos_below.y - 0.1
|
||
|
local node_below = minetest.get_node(pos_below).name
|
||
|
local nodedef = minetest.registered_nodes[node_below]
|
||
|
local touching_ground = not nodedef or -- unknown nodes are solid
|
||
|
nodedef.walkable or false
|
||
|
local liquid_below = not touching_ground and nodedef.liquidtype ~= "none"
|
||
|
return touching_ground, liquid_below
|
||
|
end
|
||
|
|
||
|
local function heli_control(self, dtime, touching_ground, liquid_below, vel_before)
|
||
|
local driver = minetest.get_player_by_name(self.driver_name)
|
||
|
if not driver then
|
||
|
-- there is no driver (eg. because driver left)
|
||
|
self.driver_name = nil
|
||
|
if self.sound_handle then
|
||
|
minetest.sound_stop(self.sound_handle)
|
||
|
self.sound_handle = nil
|
||
|
end
|
||
|
self.object:set_animation_frame_speed(0)
|
||
|
-- gravity
|
||
|
self.object:set_acceleration(vector.multiply(vector_up, -gravity))
|
||
|
return
|
||
|
end
|
||
|
|
||
|
local ctrl = driver:get_player_control()
|
||
|
local rot = self.object:get_rotation()
|
||
|
|
||
|
local vert_vel_goal = 0
|
||
|
if not liquid_below then
|
||
|
if ctrl.jump then
|
||
|
vert_vel_goal = vert_vel_goal + wanted_vert_speed
|
||
|
end
|
||
|
if ctrl.sneak then
|
||
|
vert_vel_goal = vert_vel_goal - wanted_vert_speed
|
||
|
end
|
||
|
else
|
||
|
vert_vel_goal = wanted_vert_speed
|
||
|
end
|
||
|
|
||
|
-- rotation
|
||
|
if not touching_ground then
|
||
|
local tilting_goal = vector.new()
|
||
|
if ctrl.up then
|
||
|
tilting_goal.z = tilting_goal.z + 1
|
||
|
end
|
||
|
if ctrl.down then
|
||
|
tilting_goal.z = tilting_goal.z - 1
|
||
|
end
|
||
|
if ctrl.right then
|
||
|
tilting_goal.x = tilting_goal.x + 1
|
||
|
end
|
||
|
if ctrl.left then
|
||
|
tilting_goal.x = tilting_goal.x - 1
|
||
|
end
|
||
|
tilting_goal = vector.multiply(vector.normalize(tilting_goal), tilting_max)
|
||
|
|
||
|
-- tilting
|
||
|
if vector_length_sq(vector.subtract(tilting_goal, self.tilting)) > (dtime * tilting_speed)^2 then
|
||
|
self.tilting = vector.add(self.tilting,
|
||
|
vector.multiply(vector.direction(self.tilting, tilting_goal), dtime * tilting_speed))
|
||
|
else
|
||
|
self.tilting = tilting_goal
|
||
|
end
|
||
|
if vector_length_sq(self.tilting) > tilting_max^2 then
|
||
|
self.tilting = vector.multiply(vector.normalize(self.tilting), tilting_max)
|
||
|
end
|
||
|
local new_up = vector.new(self.tilting)
|
||
|
new_up.y = 1
|
||
|
new_up = vector.normalize(new_up) -- this is what vector_up should be after the rotation
|
||
|
local new_right = vector.cross(new_up, vector_forward)
|
||
|
local new_forward = vector.cross(new_right, new_up)
|
||
|
local rot_mat = matrix3.new(
|
||
|
new_right.x, new_up.x, new_forward.x,
|
||
|
new_right.y, new_up.y, new_forward.y,
|
||
|
new_right.z, new_up.z, new_forward.z
|
||
|
)
|
||
|
rot = matrix3.to_pitch_yaw_roll(rot_mat)
|
||
|
|
||
|
rot.y = driver:get_look_horizontal()
|
||
|
|
||
|
else
|
||
|
rot.x = 0
|
||
|
rot.z = 0
|
||
|
self.tilting.x = 0
|
||
|
self.tilting.z = 0
|
||
|
end
|
||
|
|
||
|
self.object:set_rotation(rot)
|
||
|
|
||
|
-- calculate how strong the heli should accelerate towards rotated up
|
||
|
local power = vert_vel_goal - vel_before.y + gravity * dtime
|
||
|
power = math.min(math.max(power, power_min * dtime), power_max * dtime)
|
||
|
local rotated_up = matrix3.multiply(matrix3.from_pitch_yaw_roll(rot), vector_up)
|
||
|
local added_vel = vector.multiply(rotated_up, power)
|
||
|
added_vel = vector.add(added_vel, vector.multiply(vector_up, -gravity * dtime))
|
||
|
return vector.add(vel_before, added_vel)
|
||
|
end
|
||
|
|
||
|
--
|
||
|
-- entity
|
||
|
--
|
||
|
|
||
|
minetest.register_entity("helicopter:heli", {
|
||
|
initial_properties = {
|
||
|
physical = true,
|
||
|
collide_with_objects = true,
|
||
|
collisionbox = {-1,0,-1, 1,0.3,1},
|
||
|
selectionbox = {-1,0,-1, 1,0.3,1},
|
||
|
visual = "mesh",
|
||
|
mesh = "helicopter_heli.x",
|
||
|
textures = {"helicopter_blades.png", "helicopter_blades.png",
|
||
|
"helicopter_heli.png", "helicopter_glass.png"},
|
||
|
},
|
||
|
|
||
|
driver_name = nil,
|
||
|
sound_handle = nil,
|
||
|
tilting = vector.new(),
|
||
|
|
||
|
on_activate = function(self)
|
||
|
-- set the animation once and later only change the speed
|
||
|
self.object:set_animation({x = 0, y = 11}, 0, 0, true)
|
||
|
|
||
|
self.object:set_armor_groups({immortal=1})
|
||
|
|
||
|
self.object:set_acceleration(vector.multiply(vector_up, -gravity))
|
||
|
end,
|
||
|
|
||
|
on_step = function(self, dtime)
|
||
|
local touching_ground, liquid_below
|
||
|
|
||
|
local vel = self.object:get_velocity()
|
||
|
|
||
|
if self.driver_name then
|
||
|
touching_ground, liquid_below = check_node_below(self.object)
|
||
|
vel = heli_control(self, dtime, touching_ground, liquid_below, vel) or vel
|
||
|
end
|
||
|
|
||
|
if vel.x == 0 and vel.y == 0 and vel.z == 0 then
|
||
|
return
|
||
|
end
|
||
|
|
||
|
if touching_ground == nil then
|
||
|
touching_ground, liquid_below = check_node_below(self.object)
|
||
|
end
|
||
|
|
||
|
-- quadratic and constant deceleration
|
||
|
local speedsq = vector_length_sq(vel)
|
||
|
local fq, fc
|
||
|
if touching_ground then
|
||
|
fq, fc = friction_land_quadratic, friction_land_constant
|
||
|
elseif liquid_below then
|
||
|
fq, fc = friction_water_quadratic, friction_water_constant
|
||
|
else
|
||
|
fq, fc = friction_air_quadratic, friction_air_constant
|
||
|
end
|
||
|
vel = vector.apply(vel, function(a)
|
||
|
local s = math.sign(a)
|
||
|
a = math.abs(a)
|
||
|
a = math.max(0, a - fq * dtime * speedsq - fc * dtime)
|
||
|
return a * s
|
||
|
end)
|
||
|
|
||
|
self.object:set_velocity(vel)
|
||
|
end,
|
||
|
|
||
|
on_punch = function(self, puncher)
|
||
|
if not puncher or not puncher:is_player() then
|
||
|
return
|
||
|
end
|
||
|
local name = puncher:get_player_name()
|
||
|
if self.driver_name and self.driver_name ~= name then
|
||
|
-- do not allow other players to remove the object while there is a driver
|
||
|
return
|
||
|
end
|
||
|
|
||
|
if self.sound_handle then
|
||
|
minetest.sound_stop(self.sound_handle)
|
||
|
self.sound_handle = nil
|
||
|
end
|
||
|
if self.driver_name then
|
||
|
-- detach the driver first (puncher must be driver)
|
||
|
puncher:set_detach()
|
||
|
player_api.player_attached[name] = nil
|
||
|
-- player should stand again
|
||
|
player_api.set_animation(puncher, "stand")
|
||
|
self.driver_name = nil
|
||
|
end
|
||
|
|
||
|
self.object:remove()
|
||
|
|
||
|
minetest.handle_node_drops(self.object:get_pos(), {"tutorial:heli"}, puncher)
|
||
|
end,
|
||
|
|
||
|
on_rightclick = function(self, clicker)
|
||
|
if not clicker or not clicker:is_player() then
|
||
|
return
|
||
|
end
|
||
|
|
||
|
local name = clicker:get_player_name()
|
||
|
|
||
|
if name == self.driver_name then
|
||
|
-- driver clicked the object => driver gets off the vehicle
|
||
|
self.driver_name = nil
|
||
|
-- sound and animation
|
||
|
minetest.sound_stop(self.sound_handle)
|
||
|
self.sound_handle = nil
|
||
|
self.object:set_animation_frame_speed(0)
|
||
|
-- detach the player
|
||
|
clicker:set_detach()
|
||
|
player_api.player_attached[name] = nil
|
||
|
-- player should stand again
|
||
|
player_api.set_animation(clicker, "stand")
|
||
|
-- gravity
|
||
|
self.object:set_acceleration(vector.multiply(vector_up, -gravity))
|
||
|
|
||
|
elseif not self.driver_name then
|
||
|
-- no driver => clicker is new driver
|
||
|
self.driver_name = name
|
||
|
-- sound and animation
|
||
|
self.sound_handle = minetest.sound_play({name = "helicopter_motor"},
|
||
|
{object = self.object, gain = 2.0, max_hear_distance = 32, loop = true,})
|
||
|
self.object:set_animation_frame_speed(30)
|
||
|
-- attach the driver
|
||
|
clicker:set_attach(self.object, "", {x = 0, y = 6.7, z = -4}, {x = 0, y = 0, z = 0})
|
||
|
player_api.player_attached[name] = true
|
||
|
-- make the driver sit
|
||
|
minetest.after(0.2, function()
|
||
|
local player = minetest.get_player_by_name(name)
|
||
|
if player then
|
||
|
player_api.set_animation(player, "sit")
|
||
|
end
|
||
|
end)
|
||
|
-- disable gravity
|
||
|
self.object:set_acceleration(vector.new())
|
||
|
end
|
||
|
end,
|
||
|
})
|