From 62a155606e1399238b9d695ef0f32d906d1b945e Mon Sep 17 00:00:00 2001 From: Alexsandro Percy Date: Sun, 11 Jun 2023 17:29:08 -0300 Subject: [PATCH] added generic plane logic with parameters --- init.lua | 27 +- lib_planes/control.lua | 252 ++++++++++++ lib_planes/custom_physics.lua | 43 +++ lib_planes/entities.lua | 617 ++++++++++++++++++++++++++++++ lib_planes/forms.lua | 72 ++++ lib_planes/fuel_management.lua | 57 +++ lib_planes/global_definitions.lua | 6 + lib_planes/hud.lua | 216 +++++++++++ lib_planes/init.lua | 25 ++ lib_planes/utilities.lua | 332 ++++++++++++++++ models/airutils_seat_base.b3d | Bin 0 -> 311 bytes sounds/airutils_collision.ogg | Bin 0 -> 9748 bytes sounds/airutils_touch.ogg | Bin 0 -> 17479 bytes textures/airutils_black2.png | Bin 0 -> 5411 bytes textures/airutils_brown.png | Bin 0 -> 5879 bytes textures/airutils_grey.png | Bin 0 -> 1125 bytes textures/airutils_hud_panel.png | Bin 0 -> 23055 bytes textures/airutils_ind_box.png | Bin 0 -> 4864 bytes textures/airutils_painting.png | Bin 0 -> 5541 bytes 19 files changed, 1645 insertions(+), 2 deletions(-) create mode 100755 lib_planes/control.lua create mode 100755 lib_planes/custom_physics.lua create mode 100644 lib_planes/entities.lua create mode 100644 lib_planes/forms.lua create mode 100755 lib_planes/fuel_management.lua create mode 100755 lib_planes/global_definitions.lua create mode 100755 lib_planes/hud.lua create mode 100755 lib_planes/init.lua create mode 100644 lib_planes/utilities.lua create mode 100755 models/airutils_seat_base.b3d create mode 100755 sounds/airutils_collision.ogg create mode 100644 sounds/airutils_touch.ogg create mode 100755 textures/airutils_black2.png create mode 100644 textures/airutils_brown.png create mode 100755 textures/airutils_grey.png create mode 100755 textures/airutils_hud_panel.png create mode 100755 textures/airutils_ind_box.png create mode 100755 textures/airutils_painting.png diff --git a/init.lua b/init.lua index 87ce33a..145d915 100644 --- a/init.lua +++ b/init.lua @@ -37,6 +37,7 @@ dofile(minetest.get_modpath("airutils") .. DIR_DELIM .. "airutils_wind.lua") dofile(minetest.get_modpath("airutils") .. DIR_DELIM .. "inventory_management.lua") dofile(minetest.get_modpath("airutils") .. DIR_DELIM .. "light.lua") dofile(minetest.get_modpath("airutils") .. DIR_DELIM .. "physics_lib.lua") +dofile(minetest.get_modpath("airutils") .. DIR_DELIM .. "lib_planes" .. DIR_DELIM .. "init.lua") if player_api and not minetest.settings:get_bool('airutils.disable_uniforms') then dofile(minetest.get_modpath("airutils") .. DIR_DELIM .. "pilot_skin_manager.lua") @@ -269,7 +270,9 @@ function airutils.getLiftAccel(self, velocity, accel, longit_speed, roll, curr_p if self._wing_configuration then wing_config = self._wing_configuration end --flaps! local retval = accel - if longit_speed > 1 then + local min_speed = 1; + if self._min_speed then min_speed = self._min_speed end + if longit_speed > min_speed then local angle_of_attack = math.rad(self._angle_of_attack + wing_config) --local acc = 0.8 local daoa = deg(angle_of_attack) @@ -374,8 +377,11 @@ function airutils.set_paint(self, puncher, itmstck, texture_name) --minetest.chat_send_all(color ..' '.. dump(colstr)) if colstr then airutils.paint(self, colstr, texture_name) + if self._alternate_painting_texture and self._mask_painting_texture then + airutils.paint_with_mask(self, colstr, self._alternate_painting_texture, self._mask_painting_texture) + end itmstck:set_count(itmstck:get_count()-1) - puncher:set_wielded_item(itmstck) + if puncher ~= nil then puncher:set_wielded_item(itmstck) end return true end -- end painting @@ -386,6 +392,7 @@ end --painting function airutils.paint(self, colstr, texture_name) + if not self then return end if colstr then self._color = colstr local l_textures = self.initial_properties.textures @@ -399,6 +406,22 @@ function airutils.paint(self, colstr, texture_name) end end +function airutils.paint_with_mask(self, colstr, target_texture, mask_texture) + if colstr then + self._color = colstr + self._det_color = mask_colstr + local l_textures = self.initial_properties.textures + for _, texture in ipairs(l_textures) do + local indx = texture:find(target_texture) + if indx then + --"("..target_texture.."^[mask:"..mask_texture..")" + l_textures[_] = "("..target_texture.."^[multiply:".. colstr..")^("..target_texture.."^[mask:"..mask_texture..")" + end + end + self.object:set_properties({textures=l_textures}) + end +end + function airutils.getAngleFromPositions(origin, destiny) local angle_north = math.deg(math.atan2(destiny.x - origin.x, destiny.z - origin.z)) if angle_north < 0 then angle_north = angle_north + 360 end diff --git a/lib_planes/control.lua b/lib_planes/control.lua new file mode 100755 index 0000000..0e3f99a --- /dev/null +++ b/lib_planes/control.lua @@ -0,0 +1,252 @@ +--global constants +airutils.ideal_step = 0.02 + +--[[airutils.rudder_limit = 30 +airutils.elevator_limit = 40]]-- + +dofile(minetest.get_modpath("airutils") .. DIR_DELIM .. "lib_planes" .. DIR_DELIM .. "utilities.lua") + +function airutils.powerAdjust(self,dtime,factor,dir,max_power) + local max = max_power or 100 + local add_factor = factor/2 + add_factor = add_factor * (dtime/airutils.ideal_step) --adjusting the command speed by dtime + local power_index = self._power_lever + + if dir == 1 then + if self._power_lever < max then + self._power_lever = self._power_lever + add_factor + end + if self._power_lever > max then + self._power_lever = max + end + end + if dir == -1 then + if self._power_lever > 0 then + self._power_lever = self._power_lever - add_factor + if self._power_lever < 0 then self._power_lever = 0 end + end + if self._power_lever <= 0 then + self._power_lever = 0 + end + end +end + +function airutils.control(self, dtime, hull_direction, longit_speed, longit_drag, + later_speed, later_drag, accel, player, is_flying) + --if self.driver_name == nil then return end + local retval_accel = accel + + local stop = false + local ctrl = nil + + -- player control + if player then + ctrl = player:get_player_control() + + --engine and power control + if ctrl.aux1 and self._last_time_command > 0.5 then + self._last_time_command = 0 + if self._engine_running then + self._engine_running = false + self._autopilot = false + self._power_lever = 0 --zero power + self._last_applied_power = 0 --zero engine + elseif self._engine_running == false and self._energy > 0 then + self._engine_running = true + self._last_applied_power = -1 --send signal to start + end + end + + self._acceleration = 0 + if self._engine_running then + --engine acceleration calc + local engineacc = (self._power_lever * self._max_engine_acc) / 100; + + local factor = 1 + + --increase power lever + if ctrl.jump then + airutils.powerAdjust(self, dtime, factor, 1) + end + --decrease power lever + if ctrl.sneak then + airutils.powerAdjust(self, dtime, factor, -1) + if self._power_lever <= 0 and is_flying == false then + --break + if longit_speed > 0 then + engineacc = -1 + if (longit_speed + engineacc) < 0 then + engineacc = longit_speed * -1 + end + end + if longit_speed < 0 then + engineacc = 1 + if (longit_speed + engineacc) > 0 then + engineacc = longit_speed * -1 + end + end + if abs(longit_speed) < 0.2 then + stop = true + end + end + end + --do not exceed + local max_speed = 6 + if longit_speed > max_speed then + engineacc = engineacc - (longit_speed-max_speed) + if engineacc < 0 then engineacc = 0 end + end + self._acceleration = engineacc + else + local paddleacc = 0 + if longit_speed < 1.0 then + if ctrl.jump then paddleacc = 0.5 end + end + if longit_speed > -1.0 then + if ctrl.sneak then paddleacc = -0.5 end + end + self._acceleration = paddleacc + end + + local hull_acc = vector.multiply(hull_direction,self._acceleration) + retval_accel=vector.add(retval_accel,hull_acc) + + --pitch + local pitch_cmd = 0 + if ctrl.up then pitch_cmd = 1 elseif ctrl.down then pitch_cmd = -1 end + airutils.set_pitch(self, pitch_cmd, dtime) + + -- yaw + local yaw_cmd = 0 + if ctrl.right then yaw_cmd = 1 elseif ctrl.left then yaw_cmd = -1 end + airutils.set_yaw(self, yaw_cmd, dtime) + + --I'm desperate, center all! + if ctrl.right and ctrl.left then + self._elevator_angle = 0 + self._rudder_angle = 0 + end + end + + if longit_speed > 0 then + if ctrl then + if ctrl.right or ctrl.left then + else + airutils.rudder_auto_correction(self, longit_speed, dtime) + end + else + airutils.rudder_auto_correction(self, longit_speed, dtime) + end + if airutils.elevator_auto_correction then + self._elevator_angle = airutils.elevator_auto_correction(self, longit_speed, self.dtime, self._max_speed, self._elevator_angle, self._elevator_limit, airutils.ideal_step, 100) + end + end + + return retval_accel, stop +end + +function airutils.set_pitch(self, dir, dtime) + local pitch_factor = 12 + local multiplier = pitch_factor*dtime + if dir == -1 then + --minetest.chat_send_all("cabrando") + if self._elevator_angle > 0 then pitch_factor = pitch_factor * 2 end + self._elevator_angle = math.max(self._elevator_angle-multiplier,-self._elevator_limit) + elseif dir == 1 then + --minetest.chat_send_all("picando") + if self._angle_of_attack < 0 then pitch_factor = 1 end --lets reduce the command power to avoid accidents + self._elevator_angle = math.min(self._elevator_angle+multiplier,self._elevator_limit) + end +end + +function airutils.set_yaw(self, dir, dtime) + local yaw_factor = 25 + if dir == 1 then + self._rudder_angle = math.max(self._rudder_angle-(yaw_factor*dtime),-self._rudder_limit) + elseif dir == -1 then + self._rudder_angle = math.min(self._rudder_angle+(yaw_factor*dtime),self._rudder_limit) + end +end + +function airutils.rudder_auto_correction(self, longit_speed, dtime) + local factor = 1 + if self._rudder_angle > 0 then factor = -1 end + local correction = (self._rudder_limit*(longit_speed/1000)) * factor * (dtime/airutils.ideal_step) + local before_correction = self._rudder_angle + local new_rudder_angle = self._rudder_angle + correction + if math.sign(before_correction) ~= math.sign(new_rudder_angle) then + self._rudder_angle = 0 + else + self._rudder_angle = new_rudder_angle + end +end + +--obsolete, will be removed +function getAdjustFactor(curr_y, desired_y) + local max_difference = 0.1 + local adjust_factor = 0.5 + local difference = math.abs(curr_y - desired_y) + if difference > max_difference then difference = max_difference end + return (difference * adjust_factor) / max_difference +end + +function airutils.autopilot(self, dtime, hull_direction, longit_speed, accel, curr_pos) + + local retval_accel = accel + + local max_autopilot_power = 85 + + --climb + local velocity = self.object:get_velocity() + local climb_rate = velocity.y * 1.5 + if climb_rate > 5 then climb_rate = 5 end + if climb_rate < -5 then + climb_rate = -5 + end + + self._acceleration = 0 + if self._engine_running then + --engine acceleration calc + local engineacc = (self._power_lever * airutils.max_engine_acc) / 100; + --self.engine:set_animation_frame_speed(60 + self._power_lever) + + local factor = math.abs(climb_rate * 0.1) --getAdjustFactor(curr_pos.y, self._auto_pilot_altitude) + --increase power lever + if climb_rate > 0.2 then + airutils.powerAdjust(self, dtime, factor, -1) + end + --decrease power lever + if climb_rate < 0 then + airutils.powerAdjust(self, dtime, factor, 1, max_autopilot_power) + end + --do not exceed + local max_speed = airutils.max_speed + if longit_speed > max_speed then + engineacc = engineacc - (longit_speed-max_speed) + if engineacc < 0 then engineacc = 0 end + end + self._acceleration = engineacc + end + + local hull_acc = vector.multiply(hull_direction,self._acceleration) + retval_accel=vector.add(retval_accel,hull_acc) + + --pitch + if self._angle_of_attack > self._max_attack_angle then + airutils.set_pitch(self, 1, dtime) + elseif self._angle_of_attack < self._max_attack_angle then + airutils.set_pitch(self, -1, dtime) + end + + -- yaw + airutils.set_yaw(self, 0, dtime) + + if longit_speed > 0 then + airutils.rudder_auto_correction(self, longit_speed, dtime) + if airutils.elevator_auto_correction then + self._elevator_angle = airutils.elevator_auto_correction(self, longit_speed, self.dtime, airutils.max_speed, self._elevator_angle, airutils.elevator_limit, airutils.ideal_step, 100) + end + end + + return retval_accel +end diff --git a/lib_planes/custom_physics.lua b/lib_planes/custom_physics.lua new file mode 100755 index 0000000..d20763e --- /dev/null +++ b/lib_planes/custom_physics.lua @@ -0,0 +1,43 @@ + +local min = math.min +local abs = math.abs +--local deg = math.deg + +function airutils.physics(self) + local friction = 0.99 + local vel=self.object:get_velocity() + -- dumb friction + if self.isonground and not self.isinliquid then + vel = {x=vel.x*friction, + y=vel.y, + z=vel.z*friction} + self.object:set_velocity(vel) + end + + -- bounciness + if self.springiness and self.springiness > 0 then + local vnew = vector.new(vel) + + if not self.collided then -- ugly workaround for inconsistent collisions + for _,k in ipairs({'y','z','x'}) do + if vel[k]==0 and abs(self.lastvelocity[k])> 0.1 then + vnew[k]=-self.lastvelocity[k]*self.springiness + end + end + end + + if not vector.equals(vel,vnew) then + self.collided = true + else + if self.collided then + vnew = vector.new(self.lastvelocity) + end + self.collided = false + end + + self.object:set_velocity(vnew) + end + + self.object:set_acceleration({x=0,y=airutils.gravity,z=0}) + +end diff --git a/lib_planes/entities.lua b/lib_planes/entities.lua new file mode 100644 index 0000000..f94dd0f --- /dev/null +++ b/lib_planes/entities.lua @@ -0,0 +1,617 @@ +dofile(minetest.get_modpath("airutils") .. DIR_DELIM .. "lib_planes" .. DIR_DELIM .. "global_definitions.lua") + +function lib_change_color(self, colstr) + airutils.paint(self, colstr, self._painting_texture) +end + +function airutils.get_staticdata(self) -- unloaded/unloads ... is now saved + return minetest.serialize({ + --stored_sound_handle = self.sound_handle, + stored_energy = self._energy, + stored_owner = self.owner, + stored_hp = self.hp_max, + stored_color = self._color, + stored_power_lever = self._power_lever, + stored_driver_name = self.driver_name, + stored_last_accell = self._last_accell, + stored_engine_running = self._engine_running, + stored_inv_id = self._inv_id, + }) +end + +function airutils.on_deactivate(self) + airutils.save_inventory(self) +end + +function airutils.on_activate(self, staticdata, dtime_s) + airutils.actfunc(self, staticdata, dtime_s) + if staticdata ~= "" and staticdata ~= nil then + local data = minetest.deserialize(staticdata) or {} + self._energy = data.stored_energy + self.owner = data.stored_owner + self.hp_max = data.stored_hp + self._color = data.stored_color + self._power_lever = data.stored_power_lever + self.driver_name = data.stored_driver_name + self._last_accell = data.stored_last_accell + self._engine_running = data.stored_engine_running + self._inv_id = data.stored_inv_id + --self.sound_handle = data.stored_sound_handle + --minetest.debug("loaded: ", self._energy) + if self._engine_running then + self._last_applied_power = -1 --signal to start + end + end + + if self._register_parts_method then + self._register_parts_method(self) + end + + airutils.paint(self, self._color, self._painting_texture) + if self._alternate_painting_texture and self._mask_painting_texture then + airutils.paint_with_mask(self, self._color, self._alternate_painting_texture, self._mask_painting_texture) + end + + self.object:set_armor_groups({immortal=1}) + + self.object:set_animation({x = 1, y = self._anim_frames}, 0, 0, true) + if self.wheels then + self.wheels:set_animation({x = 1, y = self._anim_frames}, 0, 0, true) + end + + local inv = minetest.get_inventory({type = "detached", name = self._inv_id}) + -- if the game was closed the inventories have to be made anew, instead of just reattached + if not inv then + airutils.create_inventory(self, self._trunk_slots) + else + self.inv = inv + end +end + +function airutils.on_step(self,dtime,colinfo) + self.dtime = math.min(dtime,0.2) + self.colinfo = colinfo + self.height = airutils.get_box_height(self) + +-- physics comes first + local vel = self.object:get_velocity() + + if colinfo then + self.isonground = colinfo.touching_ground + else + if self.lastvelocity.y==0 and vel.y==0 then + self.isonground = true + else + self.isonground = false + end + end + + self:physics() + + if self.logic then + self:logic() + end + + self.lastvelocity = self.object:get_velocity() + self.time_total=self.time_total+self.dtime +end + +function airutils.logic(self) + local velocity = self.object:get_velocity() + local curr_pos = self.object:get_pos() + + self._last_time_command = self._last_time_command + self.dtime + + if self._last_time_command > 1 then self._last_time_command = 1 end + + local player = nil + if self.driver_name then player = minetest.get_player_by_name(self.driver_name) end + local passenger = nil + if self._passenger then passenger = minetest.get_player_by_name(self._passenger) end + + if player then + local ctrl = player:get_player_control() + --------------------- + -- change the driver + --------------------- + if passenger and self._last_time_command >= 1 and self._instruction_mode == true then + if self._command_is_given == true then + if ctrl.sneak or ctrl.jump or ctrl.up or ctrl.down or ctrl.right or ctrl.left then + self._last_time_command = 0 + --take the control + airutils.transfer_control(self, false) + end + else + if ctrl.sneak == true and ctrl.jump == true then + self._last_time_command = 0 + --trasnfer the control to student + airutils.transfer_control(self, true) + end + end + end + ----------- + --autopilot + ----------- + if self._instruction_mode == false and self._last_time_command >= 1 then + if self._autopilot == true then + if ctrl.sneak or ctrl.jump or ctrl.up or ctrl.down or ctrl.right or ctrl.left then + self._last_time_command = 0 + self._autopilot = false + minetest.chat_send_player(self.driver_name," >>> Autopilot deactivated") + end + else + if ctrl.sneak == true and ctrl.jump == true then + self._last_time_command = 0 + self._autopilot = true + self._auto_pilot_altitude = curr_pos.y + minetest.chat_send_player(self.driver_name,core.colorize('#00ff00', " >>> Autopilot on")) + end + end + end + ---------------------------------- + -- shows the hud for the player + ---------------------------------- + if ctrl.up == true and ctrl.down == true and self._last_time_command >= 1 then + self._last_time_command = 0 + if self._show_hud == true then + self._show_hud = false + else + self._show_hud = true + end + end + end + + local accel_y = self.object:get_acceleration().y + local rotation = self.object:get_rotation() + local yaw = rotation.y + local newyaw=yaw + local pitch = rotation.x + local roll = rotation.z + local newroll=roll + newroll = math.floor(newroll/360) + newroll = newroll * 360 + + local hull_direction = airutils.rot_to_dir(rotation) --minetest.yaw_to_dir(yaw) + local nhdir = {x=hull_direction.z,y=0,z=-hull_direction.x} -- lateral unit vector + + local longit_speed = vector.dot(velocity,hull_direction) + self._longit_speed = longit_speed + local longit_drag = vector.multiply(hull_direction,longit_speed* + longit_speed*self._longit_drag_factor*-1*airutils.sign(longit_speed)) + local later_speed = airutils.dot(velocity,nhdir) + --minetest.chat_send_all('later_speed: '.. later_speed) + local later_drag = vector.multiply(nhdir,later_speed*later_speed* + self._later_drag_factor*-1*airutils.sign(later_speed)) + local accel = vector.add(longit_drag,later_drag) + local stop = false + + local node_bellow = airutils.nodeatpos(airutils.pos_shift(curr_pos,{y=-1.3})) + local is_flying = true + if self.colinfo then + is_flying = not self.colinfo.touching_ground + end + --if is_flying then minetest.chat_send_all('is flying') end + + local is_attached = airutils.checkAttach(self, player) + + if not is_attached then + -- for some engine error the player can be detached from the machine, so lets set him attached again + airutils.checkattachBug(self) + end + + if longit_speed == 0 and is_flying == false and is_attached == false and self._engine_running == false then + return + end + + --ajustar angulo de ataque + if longit_speed then + local percentage = math.abs(((longit_speed * 100)/(self._min_speed + 5))/100) + if percentage > 1.5 then percentage = 1.5 end + self._angle_of_attack = self._wing_angle_of_attack - ((self._elevator_angle / 20)*percentage) + if self._angle_of_attack < -0.5 then + self._angle_of_attack = -0.1 + self._elevator_angle = self._elevator_angle - 0.1 + end --limiting the negative angle]]-- + if self._angle_of_attack > 20 then + self._angle_of_attack = 20 + self._elevator_angle = self._elevator_angle + 0.1 + end --limiting the very high climb angle due to strange behavior]]-- + + --set the plane on level + if airutils.adjust_attack_angle_by_speed then + self._angle_of_attack = airutils.adjust_attack_angle_by_speed(self._angle_of_attack, 1, 6, 40, longit_speed, airutils.ideal_step, self.dtime) + end + end + + --minetest.chat_send_all(self._angle_of_attack) + + -- pitch + local newpitch = math.rad(0) + if airutils.get_plane_pitch then + newpitch = airutils.get_plane_pitch(velocity, longit_speed, self._min_speed, self._angle_of_attack) + end + + + -- adjust pitch at ground + local tail_lift_min_speed = 4 + local tail_lift_max_speed = 8 + if math.abs(longit_speed) > tail_lift_min_speed then + if math.abs(longit_speed) < tail_lift_max_speed then + --minetest.chat_send_all(math.abs(longit_speed)) + local speed_range = tail_lift_max_speed - tail_lift_min_speed + percentage = 1-((math.abs(longit_speed) - tail_lift_min_speed)/speed_range) + if percentage > 1 then percentage = 1 end + if percentage < 0 then percentage = 0 end + local angle = self._tail_angle * percentage + local calculated_newpitch = math.rad(angle) + if newpitch < calculated_newpitch then newpitch = calculated_newpitch end --ja aproveita o pitch atual se ja estiver cerrto + if newpitch > math.rad(self._tail_angle) then newpitch = math.rad(self._tail_angle) end --não queremos arrastar o cauda no chão + end + else + if math.abs(longit_speed) < tail_lift_min_speed then + newpitch = math.rad(self._tail_angle) + end + end + + -- new yaw + if math.abs(self._rudder_angle)>1.5 then + local turn_rate = math.rad(14) + local yaw_turn = self.dtime * math.rad(self._rudder_angle) * turn_rate * + airutils.sign(longit_speed) * math.abs(longit_speed/2) + newyaw = yaw + yaw_turn + end + + --roll adjust + --------------------------------- + local delta = 0.002 + if is_flying then + local roll_reference = newyaw + local sdir = minetest.yaw_to_dir(roll_reference) + local snormal = {x=sdir.z,y=0,z=-sdir.x} -- rightside, dot is negative + local prsr = airutils.dot(snormal,nhdir) + local rollfactor = -90 + local roll_rate = math.rad(10) + newroll = (prsr*math.rad(rollfactor)) * (later_speed * roll_rate) * airutils.sign(longit_speed) + + --[[local rollRotation = -self._rudder_angle * 0.1 + newroll = rollRotation]]-- + + --minetest.chat_send_all('newroll: '.. newroll) + else + delta = 0.2 + if roll > 0 then + newroll = roll - delta + if newroll < 0 then newroll = 0 end + end + if roll < 0 then + newroll = roll + delta + if newroll > 0 then newroll = 0 end + end + end + + --------------------------------- + -- end roll + + local pilot = player + if self._command_is_given and passenger then + pilot = passenger + else + self._command_is_given = false + end + + ------------------------------------------------------ + --accell calculation block + ------------------------------------------------------ + if is_attached or passenger then + if self._autopilot ~= true then + accel, stop = airutils.control(self, self.dtime, hull_direction, + longit_speed, longit_drag, later_speed, later_drag, accel, pilot, is_flying) + else + accel = airutils.autopilot(self, self.dtime, hull_direction, longit_speed, accel, curr_pos) + end + end + + --end accell + + if accel == nil then accel = {x=0,y=0,z=0} end + + --lift calculation + accel.y = accel_y + + --lets apply some bob in water + if self.isinliquid then + self._engine_running = false + local bob = airutils.minmax(airutils.dot(accel,hull_direction),0.2) -- vertical bobbing + accel.y = accel.y + bob + local max_pitch = 6 + local h_vel_compensation = (((longit_speed * 2) * 100)/max_pitch)/100 + if h_vel_compensation < 0 then h_vel_compensation = 0 end + if h_vel_compensation > max_pitch then h_vel_compensation = max_pitch end + newpitch = newpitch + (velocity.y * math.rad(max_pitch - h_vel_compensation)) + end + + local new_accel = accel + if longit_speed > 1.5 then + new_accel = airutils.getLiftAccel(self, velocity, new_accel, longit_speed, roll, curr_pos, self._lift, 15000) + end + -- end lift + + --wind effects + if longit_speed > 1.5 and airutils.wind then + local wind = airutils.get_wind(curr_pos, 0.1) + new_accel = vector.add(new_accel, wind) + end + + if stop ~= true then --maybe == nil + self._last_accell = new_accel + self.object:move_to(curr_pos) + --self.object:set_velocity(velocity) + --[[if player then + airutils.attach(self, player, self._instruction_mode) + end]]-- + airutils.set_acceleration(self.object, new_accel) + else + if stop == true then + self.object:set_acceleration({x=0,y=0,z=0}) + self.object:set_velocity({x=0,y=0,z=0}) + end + end + + if self.wheels then + if is_flying == false then --isn't flying? + --animate wheels + if math.abs(longit_speed) > 0.1 then + self.wheels:set_animation_frame_speed(longit_speed * 10) + else + self.wheels:set_animation_frame_speed(0) + end + else + --stop wheels + self.wheels:set_animation_frame_speed(0) + end + end + + + + ------------------------------------------------------ + -- end accell + ------------------------------------------------------ + + ------------------------------------------------------ + -- sound and animation + ------------------------------------------------------ + airutils.engine_set_sound_and_animation(self) + ------------------------------------------------------ + + --self.object:get_luaentity() --hack way to fix jitter on climb + + --adjust climb indicator + local climb_rate = velocity.y + if climb_rate > 5 then climb_rate = 5 end + if climb_rate < -5 then + climb_rate = -5 + end + + --is an stall, force a recover + if longit_speed < (self._min_speed / 2) and climb_rate < -3.5 and is_flying then + self._elevator_angle = 0 + self._angle_of_attack = -4 + newpitch = math.rad(self._angle_of_attack) + end + + --minetest.chat_send_all('rate '.. climb_rate) + local climb_angle = airutils.get_gauge_angle(climb_rate) + --self.climb_gauge:set_attach(self.object,'',ALBATROS_D5_GAUGE_CLIMBER_POSITION,{x=0,y=0,z=climb_angle}) + + local indicated_speed = longit_speed * 0.9 + if indicated_speed < 0 then indicated_speed = 0 end + local speed_angle = airutils.get_gauge_angle(indicated_speed, -45) + --self.speed_gauge:set_attach(self.object,'',ALBATROS_D5_GAUGE_SPEED_POSITION,{x=0,y=0,z=speed_angle}) + + if is_attached then + if self._show_hud then + airutils.update_hud(player, climb_angle, speed_angle) + else + airutils.remove_hud(player) + end + end + + --adjust power indicator + local power_indicator_angle = airutils.get_gauge_angle(self._power_lever/10) + --self.power_gauge:set_attach(self.object,'',ALBATROS_D5_GAUGE_POWER_POSITION,{x=0,y=0,z=power_indicator_angle}) + + if is_flying == false then + -- new yaw + local turn_rate = math.rad(30) + local yaw_turn = self.dtime * math.rad(self._rudder_angle) * turn_rate * + airutils.sign(longit_speed) * math.abs(longit_speed/2) + newyaw = yaw + yaw_turn + end + + --apply rotations + self.object:set_rotation({x=newpitch,y=newyaw,z=newroll}) + --end + + --adjust elevator pitch (3d model) + self.object:set_bone_position("elevator", self._elevator_pos, {x=-self._elevator_angle*2 - 90, y=0, z=0}) + --adjust rudder + self.object:set_bone_position("rudder", self._rudder_pos, {x=0,y=self._rudder_angle,z=0}) + --adjust ailerons + if self._aileron_r_pos and self._aileron_l_pos then + self.object:set_bone_position("aileron.r", self._aileron_r_pos, {x=-self._rudder_angle - 90,y=0,z=0}) + self.object:set_bone_position("aileron.l", self._aileron_l_pos, {x=self._rudder_angle - 90,y=0,z=0}) + end + --set stick position + if self.stick then + self.stick:set_attach(self.object,'',self._stick_pos,{x=self._elevator_angle/2,y=0,z=self._rudder_angle}) + end + + -- calculate energy consumption -- + airutils.consumptionCalc(self, accel) + + --test collision + airutils.testImpact(self, velocity, curr_pos) + + --saves last velocity for collision detection (abrupt stop) + self._last_vel = self.object:get_velocity() +end + +function airutils.on_punch(self, puncher, ttime, toolcaps, dir, damage) + if not puncher or not puncher:is_player() then + return + end + + local is_admin = false + is_admin = minetest.check_player_privs(puncher, {server=true}) + local name = puncher:get_player_name() + if self.owner and self.owner ~= name and self.owner ~= "" then + if is_admin == false then return end + end + if self.owner == nil then + self.owner = name + end + + 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 + + local is_attached = false + if puncher:get_attach() == self.object then is_attached = true end + + local itmstck=puncher:get_wielded_item() + local item_name = "" + if itmstck then item_name = itmstck:get_name() end + + if is_attached == false then + if airutils.loadFuel(self, puncher:get_player_name()) then + return + end + + --repair + if (item_name == "airutils:repair_tool") + and self._engine_running == false then + if self.hp_max < 50 then + local inventory_item = "default:steel_ingot" + local inv = puncher:get_inventory() + if inv:contains_item("main", inventory_item) then + local stack = ItemStack(inventory_item .. " 1") + inv:remove_item("main", stack) + self.hp_max = self.hp_max + 10 + if self.hp_max > 50 then self.hp_max = 50 end + airutils.setText(self, self.infotext) + else + minetest.chat_send_player(puncher:get_player_name(), "You need steel ingots in your inventory to perform this repair.") + end + end + return + end + + -- deal with painting or destroying + if itmstck then + + if airutils.set_paint(self, puncher, itmstck, self._painting_texture) == false then + if not self.driver and toolcaps and toolcaps.damage_groups + and toolcaps.damage_groups.fleshy and item_name ~= airutils.fuel then + --airutils.hurt(self,toolcaps.damage_groups.fleshy - 1) + --airutils.make_sound(self,'hit') + self.hp_max = self.hp_max - 10 + minetest.sound_play(self._collision_sound, { + object = self.object, + max_hear_distance = 5, + gain = 1.0, + fade = 0.0, + pitch = 1.0, + }) + airutils.setText(self, self.infotext) + end + end + end + + if self.hp_max <= 0 then + airutils.destroy(self) + end + + end + +end + +function airutils.on_rightclick(self, clicker) + if not clicker or not clicker:is_player() then + return + end + + local name = clicker:get_player_name() + + if self.owner == "" then + self.owner = name + end + + local passenger_name = nil + if self._passenger then + passenger_name = self._passenger + end + + local touching_ground, liquid_below = airutils.check_node_below(self.object, 1.3) + local is_on_ground = self.isinliquid or touching_ground or liquid_below + local is_under_water = airutils.check_is_under_water(self.object) + + --minetest.chat_send_all('name '.. dump(name) .. ' - pilot: ' .. dump(self.driver_name) .. ' - pax: ' .. dump(passenger_name)) + --========================= + -- detach pilot + --========================= + if name == self.driver_name then + airutils.pilot_formspec(name) + --========================= + -- detach passenger + --========================= + elseif name == passenger_name then + if is_on_ground or clicker:get_player_control().sneak then + airutils.dettach_pax(self, clicker) + else + minetest.chat_send_player(name, "Hold sneak and right-click to disembark while flying") + end + + --========================= + -- attach pilot + --========================= + elseif not self.driver_name then + if self.owner == name or minetest.check_player_privs(clicker, {protection_bypass=true}) then + if clicker:get_player_control().aux1 == true then --lets see the inventory + airutils.show_vehicle_trunk_formspec(self, clicker, airutils.trunk_slots) + else + if is_under_water then return end + --remove pax to prevent bug + if self._passenger then + local pax_obj = minetest.get_player_by_name(self._passenger) + airutils.dettach_pax(self, pax_obj) + end + + --attach player + if clicker:get_player_control().sneak == true then + -- flight instructor mode + self._instruction_mode = true + airutils.attach(self, clicker, true) + else + -- no driver => clicker is new driver + self._instruction_mode = false + airutils.attach(self, clicker) + end + self._elevator_angle = 0 + self._rudder_angle = 0 + self._command_is_given = false + end + else + minetest.chat_send_player(name, core.colorize('#ff0000', " >>> You aren't the owner of this machine.")) + end + + --========================= + -- attach passenger + --========================= + elseif self.driver_name and not self._passenger then + airutils.attach_pax(self, clicker) + + else + minetest.chat_send_player(name, core.colorize('#ff0000', " >>> Can't enter airplane.")) + end +end diff --git a/lib_planes/forms.lua b/lib_planes/forms.lua new file mode 100644 index 0000000..7f1d547 --- /dev/null +++ b/lib_planes/forms.lua @@ -0,0 +1,72 @@ +dofile(minetest.get_modpath("airutils") .. DIR_DELIM .. "lib_planes" .. DIR_DELIM .. "global_definitions.lua") + +-------------- +-- Manual -- +-------------- + +function airutils.getPlaneFromPlayer(player) + local seat = player:get_attach() + if seat then + local plane = seat:get_attach() + return plane + end + return nil +end + +function airutils.pilot_formspec(name) + local basic_form = table.concat({ + "formspec_version[3]", + "size[6,6]", + }, "") + + basic_form = basic_form.."button[1,1.0;4,1;go_out;Go Offboard]" + basic_form = basic_form.."button[1,2.5;4,1;hud;Show/Hide Gauges]" + + minetest.show_formspec(name, "lib_planes:pilot_main", basic_form) +end + +minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname == "lib_planes:pilot_main" then + local name = player:get_player_name() + local plane_obj = airutils.getPlaneFromPlayer(player) + if plane_obj then + local ent = plane_obj:get_luaentity() + if fields.hud then + if ent._show_hud == true then + ent._show_hud = false + else + ent._show_hud = true + end + end + if fields.go_out then + local touching_ground, liquid_below = airutils.check_node_below(plane_obj, 1.3) + local is_on_ground = ent.isinliquid or touching_ground or liquid_below + + if is_on_ground then --or clicker:get_player_control().sneak then + if ent._passenger then --any pax? + local pax_obj = minetest.get_player_by_name(ent._passenger) + airutils.dettach_pax(ent, pax_obj) + end + ent._instruction_mode = false + --[[ sound and animation + if ent.sound_handle then + minetest.sound_stop(ent.sound_handle) + ent.sound_handle = nil + end + ent.engine:set_animation_frame_speed(0)]]-- + else + -- not on ground + if ent._passenger then + --give the control to the pax + ent._autopilot = false + airutils.transfer_control(ent, true) + ent._command_is_given = true + ent._instruction_mode = true + end + end + airutils.dettachPlayer(ent, player) + end + end + minetest.close_formspec(name, "lib_planes:pilot_main") + end +end) diff --git a/lib_planes/fuel_management.lua b/lib_planes/fuel_management.lua new file mode 100755 index 0000000..78d5f4c --- /dev/null +++ b/lib_planes/fuel_management.lua @@ -0,0 +1,57 @@ +function airutils.contains(table, val) + for k,v in pairs(table) do + if k == val then + return v + end + end + return false +end + +function airutils.loadFuel(self, player_name) + local player = minetest.get_player_by_name(player_name) + local inv = player:get_inventory() + + local itmstck=player:get_wielded_item() + local item_name = "" + if itmstck then item_name = itmstck:get_name() end + + local fuel = airutils.contains(airutils.fuel, item_name) + if fuel then + local stack = ItemStack(item_name .. " 1") + + if self._energy < 10 then + inv:remove_item("main", stack) + self._energy = self._energy + fuel + if self._energy > 10 then self._energy = 10 end + + local energy_indicator_angle = airutils.get_gauge_angle(self._energy) + --self.fuel_gauge:set_attach(self.object,'',self._gauge_fuel_position,{x=0,y=0,z=energy_indicator_angle}) + end + + return true + end + + return false +end + +function airutils.consumptionCalc(self, accel) + if accel == nil then return end + if self._energy > 0 and self._engine_running and accel ~= nil then + local consumed_power = self._power_lever/700000 + --minetest.chat_send_all('consumed: '.. consumed_power) + self._energy = self._energy - consumed_power; + + local energy_indicator_angle = airutils.get_gauge_angle(self._energy) + if self.fuel_gauge then + if self.fuel_gauge:get_luaentity() then + self.fuel_gauge:set_attach(self.object,'',self._gauge_fuel_position,{x=0,y=0,z=energy_indicator_angle}) + end + end + end + if self._energy <= 0 and self._engine_running and accel ~= nil then + self._engine_running = false + self._autopilot = false + if self.sound_handle then minetest.sound_stop(self.sound_handle) end + self.object:set_animation_frame_speed(0) + end +end diff --git a/lib_planes/global_definitions.lua b/lib_planes/global_definitions.lua new file mode 100755 index 0000000..ecf81a9 --- /dev/null +++ b/lib_planes/global_definitions.lua @@ -0,0 +1,6 @@ +-- +-- constants +-- +airutils.vector_up = vector.new(0, 1, 0) + + diff --git a/lib_planes/hud.lua b/lib_planes/hud.lua new file mode 100755 index 0000000..dc38053 --- /dev/null +++ b/lib_planes/hud.lua @@ -0,0 +1,216 @@ +airutils.hud_list = {} + +function airutils.animate_gauge(player, ids, prefix, x, y, angle) + local angle_in_rad = math.rad(angle + 180) + local dim = 10 + local pos_x = math.sin(angle_in_rad) * dim + local pos_y = math.cos(angle_in_rad) * dim + player:hud_change(ids[prefix .. "2"], "offset", {x = pos_x + x, y = pos_y + y}) + dim = 20 + pos_x = math.sin(angle_in_rad) * dim + pos_y = math.cos(angle_in_rad) * dim + player:hud_change(ids[prefix .. "3"], "offset", {x = pos_x + x, y = pos_y + y}) + dim = 30 + pos_x = math.sin(angle_in_rad) * dim + pos_y = math.cos(angle_in_rad) * dim + player:hud_change(ids[prefix .. "4"], "offset", {x = pos_x + x, y = pos_y + y}) + dim = 40 + pos_x = math.sin(angle_in_rad) * dim + pos_y = math.cos(angle_in_rad) * dim + player:hud_change(ids[prefix .. "5"], "offset", {x = pos_x + x, y = pos_y + y}) + dim = 50 + pos_x = math.sin(angle_in_rad) * dim + pos_y = math.cos(angle_in_rad) * dim + player:hud_change(ids[prefix .. "6"], "offset", {x = pos_x + x, y = pos_y + y}) + dim = 60 + pos_x = math.sin(angle_in_rad) * dim + pos_y = math.cos(angle_in_rad) * dim + player:hud_change(ids[prefix .. "7"], "offset", {x = pos_x + x, y = pos_y + y}) +end + +function airutils.update_hud(player, climb, speed) + local player_name = player:get_player_name() + + local screen_pos_y = -150 + local screen_pos_x = 10 + + local clb_gauge_x = screen_pos_x + 80 + local clb_gauge_y = screen_pos_y + 5 + local sp_gauge_x = screen_pos_x + 180 + local sp_gauge_y = clb_gauge_y + + local ids = airutils.hud_list[player_name] + if ids then + airutils.animate_gauge(player, ids, "clb_pt_", clb_gauge_x, clb_gauge_y, climb) + airutils.animate_gauge(player, ids, "sp_pt_", sp_gauge_x, sp_gauge_y, speed) + else + ids = {} + + ids["title"] = player:hud_add({ + hud_elem_type = "text", + position = {x = 0, y = 1}, + offset = {x = screen_pos_x +140, y = screen_pos_y -100}, + text = "Flight Information", + alignment = 0, + scale = { x = 100, y = 30}, + number = 0xFFFFFF, + }) + + ids["bg"] = player:hud_add({ + hud_elem_type = "image", + position = {x = 0, y = 1}, + offset = {x = screen_pos_x, y = screen_pos_y}, + text = "airutils_hud_panel.png", + scale = { x = 0.5, y = 0.5}, + alignment = { x = 1, y = 0 }, + }) + + ids["clb_pt_1"] = player:hud_add({ + hud_elem_type = "image", + position = {x = 0, y = 1}, + offset = {x = clb_gauge_x, y = clb_gauge_y}, + text = "airutils_ind_box.png", + scale = { x = 6, y = 6}, + alignment = { x = 1, y = 0 }, + }) + + ids["clb_pt_2"] = player:hud_add({ + hud_elem_type = "image", + position = {x = 0, y = 1}, + offset = {x = clb_gauge_x, y = clb_gauge_y}, + text = "airutils_ind_box.png", + scale = { x = 6, y = 6}, + alignment = { x = 1, y = 0 }, + }) + ids["clb_pt_3"] = player:hud_add({ + hud_elem_type = "image", + position = {x = 0, y = 1}, + offset = {x = clb_gauge_x, y = clb_gauge_y}, + text = "airutils_ind_box.png", + scale = { x = 6, y = 6}, + alignment = { x = 1, y = 0 }, + }) + ids["clb_pt_4"] = player:hud_add({ + hud_elem_type = "image", + position = {x = 0, y = 1}, + offset = {x = clb_gauge_x, y = clb_gauge_y}, + text = "airutils_ind_box.png", + scale = { x = 6, y = 6}, + alignment = { x = 1, y = 0 }, + }) + ids["clb_pt_5"] = player:hud_add({ + hud_elem_type = "image", + position = {x = 0, y = 1}, + offset = {x = clb_gauge_x, y = clb_gauge_y}, + text = "airutils_ind_box.png", + scale = { x = 6, y = 6}, + alignment = { x = 1, y = 0 }, + }) + ids["clb_pt_6"] = player:hud_add({ + hud_elem_type = "image", + position = {x = 0, y = 1}, + offset = {x = clb_gauge_x, y = clb_gauge_y}, + text = "airutils_ind_box.png", + scale = { x = 6, y = 6}, + alignment = { x = 1, y = 0 }, + }) + ids["clb_pt_7"] = player:hud_add({ + hud_elem_type = "image", + position = {x = 0, y = 1}, + offset = {x = clb_gauge_x, y = clb_gauge_y}, + text = "airutils_ind_box.png", + scale = { x = 6, y = 6}, + alignment = { x = 1, y = 0 }, + }) + + ids["sp_pt_1"] = player:hud_add({ + hud_elem_type = "image", + position = {x = 0, y = 1}, + offset = {x = sp_gauge_x, y = sp_gauge_y}, + text = "airutils_ind_box.png", + scale = { x = 6, y = 6}, + alignment = { x = 1, y = 0 }, + }) + ids["sp_pt_2"] = player:hud_add({ + hud_elem_type = "image", + position = {x = 0, y = 1}, + offset = {x = sp_gauge_x, y = sp_gauge_y}, + text = "airutils_ind_box.png", + scale = { x = 6, y = 6}, + alignment = { x = 1, y = 0 }, + }) + ids["sp_pt_3"] = player:hud_add({ + hud_elem_type = "image", + position = {x = 0, y = 1}, + offset = {x = sp_gauge_x, y = sp_gauge_y}, + text = "airutils_ind_box.png", + scale = { x = 6, y = 6}, + alignment = { x = 1, y = 0 }, + }) + ids["sp_pt_4"] = player:hud_add({ + hud_elem_type = "image", + position = {x = 0, y = 1}, + offset = {x = sp_gauge_x, y = sp_gauge_y}, + text = "airutils_ind_box.png", + scale = { x = 6, y = 6}, + alignment = { x = 1, y = 0 }, + }) + ids["sp_pt_5"] = player:hud_add({ + hud_elem_type = "image", + position = {x = 0, y = 1}, + offset = {x = sp_gauge_x, y = sp_gauge_y}, + text = "airutils_ind_box.png", + scale = { x = 6, y = 6}, + alignment = { x = 1, y = 0 }, + }) + ids["sp_pt_6"] = player:hud_add({ + hud_elem_type = "image", + position = {x = 0, y = 1}, + offset = {x = sp_gauge_x, y = sp_gauge_y}, + text = "airutils_ind_box.png", + scale = { x = 6, y = 6}, + alignment = { x = 1, y = 0 }, + }) + ids["sp_pt_7"] = player:hud_add({ + hud_elem_type = "image", + position = {x = 0, y = 1}, + offset = {x = sp_gauge_x, y = sp_gauge_y}, + text = "airutils_ind_box.png", + scale = { x = 6, y = 6}, + alignment = { x = 1, y = 0 }, + }) + + airutils.hud_list[player_name] = ids + end +end + + +function airutils.remove_hud(player) + if player then + local player_name = player:get_player_name() + --minetest.chat_send_all(player_name) + local ids = airutils.hud_list[player_name] + if ids then + --player:hud_remove(ids["altitude"]) + --player:hud_remove(ids["time"]) + player:hud_remove(ids["title"]) + player:hud_remove(ids["bg"]) + player:hud_remove(ids["clb_pt_7"]) + player:hud_remove(ids["clb_pt_6"]) + player:hud_remove(ids["clb_pt_5"]) + player:hud_remove(ids["clb_pt_4"]) + player:hud_remove(ids["clb_pt_3"]) + player:hud_remove(ids["clb_pt_2"]) + player:hud_remove(ids["clb_pt_1"]) + player:hud_remove(ids["sp_pt_7"]) + player:hud_remove(ids["sp_pt_6"]) + player:hud_remove(ids["sp_pt_5"]) + player:hud_remove(ids["sp_pt_4"]) + player:hud_remove(ids["sp_pt_3"]) + player:hud_remove(ids["sp_pt_2"]) + player:hud_remove(ids["sp_pt_1"]) + end + airutils.hud_list[player_name] = nil + end + +end diff --git a/lib_planes/init.lua b/lib_planes/init.lua new file mode 100755 index 0000000..e7847c2 --- /dev/null +++ b/lib_planes/init.lua @@ -0,0 +1,25 @@ + + +dofile(minetest.get_modpath("airutils") .. DIR_DELIM .. "lib_planes" .. DIR_DELIM .. "global_definitions.lua") +dofile(minetest.get_modpath("airutils") .. DIR_DELIM .. "lib_planes" .. DIR_DELIM .. "control.lua") +dofile(minetest.get_modpath("airutils") .. DIR_DELIM .. "lib_planes" .. DIR_DELIM .. "fuel_management.lua") +dofile(minetest.get_modpath("airutils") .. DIR_DELIM .. "lib_planes" .. DIR_DELIM .. "custom_physics.lua") +dofile(minetest.get_modpath("airutils") .. DIR_DELIM .. "lib_planes" .. DIR_DELIM .. "utilities.lua") +dofile(minetest.get_modpath("airutils") .. DIR_DELIM .. "lib_planes" .. DIR_DELIM .. "entities.lua") +dofile(minetest.get_modpath("airutils") .. DIR_DELIM .. "lib_planes" .. DIR_DELIM .. "forms.lua") + +-- +-- helpers and co. +-- + + +-- +-- items +-- + +settings = Settings(minetest.get_worldpath() .. "/settings.conf") +local function fetch_setting(name) + local sname = name + return settings and settings:get(sname) or minetest.settings:get(sname) +end + diff --git a/lib_planes/utilities.lua b/lib_planes/utilities.lua new file mode 100644 index 0000000..d027d8b --- /dev/null +++ b/lib_planes/utilities.lua @@ -0,0 +1,332 @@ +dofile(minetest.get_modpath("airutils") .. DIR_DELIM .. "lib_planes" .. DIR_DELIM .. "global_definitions.lua") +dofile(minetest.get_modpath("airutils") .. DIR_DELIM .. "lib_planes" .. DIR_DELIM .. "hud.lua") + +function airutils.properties_copy(origin_table) + local tablecopy = {} + for k, v in pairs(origin_table) do + tablecopy[k] = v + end + return tablecopy +end + +function airutils.get_hipotenuse_value(point1, point2) + return math.sqrt((point1.x - point2.x) ^ 2 + (point1.y - point2.y) ^ 2 + (point1.z - point2.z) ^ 2) +end + +function airutils.dot(v1,v2) + return v1.x*v2.x+v1.y*v2.y+v1.z*v2.z +end + +function airutils.sign(n) + return n>=0 and 1 or -1 +end + +function airutils.minmax(v,m) + return math.min(math.abs(v),m)*airutils.sign(v) +end + +function airutils.get_gauge_angle(value, initial_angle) + initial_angle = initial_angle or 90 + local angle = value * 18 + angle = angle - initial_angle + angle = angle * -1 + return angle +end + +-- attach player +function airutils.attach(self, player, instructor_mode) + instructor_mode = instructor_mode or false + local name = player:get_player_name() + self.driver_name = name + + -- attach the driver + local eye_y = 0 + if instructor_mode == true then + eye_y = -2.5 + player:set_attach(self.passenger_seat_base, "", {x = 0, y = 0, z = 0}, {x = 0, y = 0, z = 0}) + else + eye_y = -4 + player:set_attach(self.pilot_seat_base, "", {x = 0, y = 0, z = 0}, {x = 0, y = 0, z = 0}) + end + if airutils.detect_player_api(player) == 1 then + eye_y = eye_y + 6.5 + end + + player:set_eye_offset({x = 0, y = eye_y, z = 2}, {x = 0, y = 1, z = -30}) + player_api.player_attached[name] = true + player_api.set_animation(player, "sit") + -- make the driver sit + minetest.after(1, function() + if player then + --minetest.chat_send_all("okay") + airutils.sit(player) + --apply_physics_override(player, {speed=0,gravity=0,jump=0}) + end + end) +end + +-- attach passenger +function airutils.attach_pax(self, player) + local name = player:get_player_name() + self._passenger = name + + -- attach the driver + local eye_y = 0 + if self._instruction_mode == true then + eye_y = -4 + player:set_attach(self.pilot_seat_base, "", {x = 0, y = 0, z = 0}, {x = 0, y = 0, z = 0}) + else + eye_y = -2.5 + player:set_attach(self.passenger_seat_base, "", {x = 0, y = 0, z = 0}, {x = 0, y = 0, z = 0}) + end + if airutils.detect_player_api(player) == 1 then + eye_y = eye_y + 6.5 + end + + player:set_eye_offset({x = 0, y = eye_y, z = 2}, {x = 0, y = 1, z = -30}) + player_api.player_attached[name] = true + player_api.set_animation(player, "sit") + -- make the driver sit + minetest.after(1, function() + player = minetest.get_player_by_name(name) + if player then + airutils.sit(player) + --apply_physics_override(player, {speed=0,gravity=0,jump=0}) + end + end) +end + +function airutils.dettachPlayer(self, player) + local name = self.driver_name + airutils.setText(self, self.infotext) + + airutils.remove_hud(player) + + --self._engine_running = false + + -- driver clicked the object => driver gets off the vehicle + self.driver_name = nil + + -- detach the player + --player:set_physics_override({speed = 1, jump = 1, gravity = 1, sneak = true}) + player:set_detach() + player_api.player_attached[name] = nil + player:set_eye_offset({x=0,y=0,z=0},{x=0,y=0,z=0}) + player_api.set_animation(player, "stand") + self.driver = nil + --remove_physics_override(player, {speed=1,gravity=1,jump=1}) +end + +function airutils.dettach_pax(self, player) + local name = self._passenger + + -- passenger clicked the object => driver gets off the vehicle + self._passenger = nil + + -- detach the player + --player:set_physics_override({speed = 1, jump = 1, gravity = 1, sneak = true}) + if player then + player:set_detach() + player_api.player_attached[name] = nil + player_api.set_animation(player, "stand") + player:set_eye_offset({x=0,y=0,z=0},{x=0,y=0,z=0}) + --remove_physics_override(player, {speed=1,gravity=1,jump=1}) + end +end + +function airutils.checkAttach(self, player) + if player then + local player_attach = player:get_attach() + if player_attach then + if player_attach == self.pilot_seat_base or player_attach == self.passenger_seat_base then + return true + end + end + end + return false +end + +-- destroy the boat +function airutils.destroy(self) + if self.sound_handle then + minetest.sound_stop(self.sound_handle) + self.sound_handle = nil + end + + if self._passenger then + -- detach the passenger + local passenger = minetest.get_player_by_name(self._passenger) + if passenger then + airutils.dettach_pax(self, passenger) + end + end + + if self.driver_name then + -- detach the driver + local player = minetest.get_player_by_name(self.driver_name) + airutils.dettachPlayer(self, player) + end + + local pos = self.object:get_pos() + if self.fuel_gauge then self.fuel_gauge:remove() end + if self.power_gauge then self.power_gauge:remove() end + if self.climb_gauge then self.climb_gauge:remove() end + if self.speed_gauge then self.speed_gauge:remove() end + if self.engine then self.engine:remove() end + if self.pilot_seat_base then self.pilot_seat_base:remove() end + if self.passenger_seat_base then self.passenger_seat_base:remove() end + + if self.stick then self.stick:remove() end + + airutils.destroy_inventory(self) + self.object:remove() + + --[[pos.y=pos.y+2 + minetest.add_item({x=pos.x+math.random()-0.5,y=pos.y,z=pos.z+math.random()-0.5},'hidroplane:wings') + + for i=1,6 do + minetest.add_item({x=pos.x+math.random()-0.5,y=pos.y,z=pos.z+math.random()-0.5},'default:steel_ingot') + end + + for i=1,2 do + minetest.add_item({x=pos.x+math.random()-0.5,y=pos.y,z=pos.z+math.random()-0.5},'wool:white') + end + + for i=1,6 do + minetest.add_item({x=pos.x+math.random()-0.5,y=pos.y,z=pos.z+math.random()-0.5},'default:mese_crystal') + minetest.add_item({x=pos.x+math.random()-0.5,y=pos.y,z=pos.z+math.random()-0.5},'default:diamond') + end]]-- + + --minetest.add_item({x=pos.x+math.random()-0.5,y=pos.y,z=pos.z+math.random()-0.5},'hidroplane:hidro') +end + +function airutils.testImpact(self, velocity, position) + local p = position --self.object:get_pos() + local collision = false + if self._last_vel == nil then return end + --lets calculate the vertical speed, to avoid the bug on colliding on floor with hard lag + if abs(velocity.y - self._last_vel.y) > 2 then + local noded = airutils.nodeatpos(airutils.pos_shift(p,{y=-2.8})) + if (noded and noded.drawtype ~= 'airlike') then + collision = true + else + self.object:set_velocity(self._last_vel) + --self.object:set_acceleration(self._last_accell) + self.object:set_velocity(vector.add(velocity, vector.multiply(self._last_accell, self.dtime/8))) + end + end + local impact = abs(airutils.get_hipotenuse_value(velocity, self._last_vel)) + --minetest.chat_send_all('impact: '.. impact .. ' - hp: ' .. self.hp_max) + if impact > 2 then + --minetest.chat_send_all('impact: '.. impact .. ' - hp: ' .. self.hp_max) + if self.colinfo then + collision = self.colinfo.collides + end + end + + if impact > 1.2 and self._longit_speed > 2 then + local noded = airutils.nodeatpos(airutils.pos_shift(p,{y=-2.8})) + if (noded and noded.drawtype ~= 'airlike') then + minetest.sound_play("airutils_touch", { + --to_player = self.driver_name, + object = self.object, + max_hear_distance = 15, + gain = 1.0, + fade = 0.0, + pitch = 1.0, + }, true) + end + end + + if collision then + --self.object:set_velocity({x=0,y=0,z=0}) + local damage = impact / 2 + self.hp_max = self.hp_max - damage --subtract the impact value directly to hp meter + minetest.sound_play(self._collision_sound, { + --to_player = self.driver_name, + object = self.object, + max_hear_distance = 15, + gain = 1.0, + fade = 0.0, + pitch = 1.0, + }, true) + + if self.driver_name then + local player_name = self.driver_name + airutils.setText(self, self.infotext) + + --minetest.chat_send_all('damage: '.. damage .. ' - hp: ' .. self.hp_max) + if self.hp_max < 0 then --if acumulated damage is greater than 50, adieu + airutils.destroy(self) + end + + local player = minetest.get_player_by_name(player_name) + if player then + if player:get_hp() > 0 then + player:set_hp(player:get_hp()-(damage/2)) + end + end + if self._passenger ~= nil then + local passenger = minetest.get_player_by_name(self._passenger) + if passenger then + if passenger:get_hp() > 0 then + passenger:set_hp(passenger:get_hp()-(damage/2)) + end + end + end + end + + end +end + +function airutils.checkattachBug(self) + -- for some engine error the player can be detached from the submarine, so lets set him attached again + if self.owner and self.driver_name then + -- attach the driver again + local player = minetest.get_player_by_name(self.owner) + if player then + if player:get_hp() > 0 then + airutils.attach(self, player, self._instruction_mode) + else + airutils.dettachPlayer(self, player) + end + else + if self._passenger ~= nil and self._command_is_given == false then + self._autopilot = false + airutils.transfer_control(self, true) + end + end + end +end + +function airutils.engineSoundPlay(self) + --sound + if self.sound_handle then minetest.sound_stop(self.sound_handle) end + if self.object then + self.sound_handle = minetest.sound_play({name = self._engine_sound}, + {object = self.object, gain = 2.0, + pitch = 0.5 + ((self._power_lever/100)/2), + max_hear_distance = 15, + loop = true,}) + end +end + +function airutils.engine_set_sound_and_animation(self) + --minetest.chat_send_all('test1 ' .. dump(self._engine_running) ) + if self._engine_running then + if self._last_applied_power ~= self._power_lever then + --minetest.chat_send_all('test2') + self._last_applied_power = self._power_lever + self.object:set_animation_frame_speed(60 + self._power_lever) + airutils.engineSoundPlay(self) + end + else + if self.sound_handle then + minetest.sound_stop(self.sound_handle) + self.sound_handle = nil + self.object:set_animation_frame_speed(0) + end + end +end + + diff --git a/models/airutils_seat_base.b3d b/models/airutils_seat_base.b3d new file mode 100755 index 0000000000000000000000000000000000000000..ccf1db3b63c52f5ffee961c2f279d43cd0feec26 GIT binary patch literal 311 zcmZ>AGIr5tWMBXy2B)CVU@agGVkhM!CTBC8J>z2!!wd`!_CN`+EQtOO1b+T5t{;JH z=gOSSyp+@;Jp%(nkQ@?duxA8n1mRCCruJWe7$)iK8tkzJ$N?D?78DZP0+j$+&jc2) z-*;NNVW0J!&s)=U3yoYSC))r2UvHOvqTUW9R=mu}4kiXu7iyem3l-Z3Qx|Cb4km_8 cFNPf9 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 KXtVn2mH!9hX=}d# literal 0 HcmV?d00001 diff --git a/sounds/airutils_touch.ogg b/sounds/airutils_touch.ogg new file mode 100644 index 0000000000000000000000000000000000000000..90d35c428fda608680896dfbaa3257332f2db61b GIT binary patch literal 17479 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 QWN9^MPHxoVbwRuS3p=+{2><{9 literal 0 HcmV?d00001 diff --git a/textures/airutils_brown.png b/textures/airutils_brown.png new file mode 100644 index 0000000000000000000000000000000000000000..ff145837f85bdeda3ad86425a8c75b76c5b5d7a2 GIT binary patch literal 5879 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 PK+t3%5y891CeHs0(y>1Z literal 0 HcmV?d00001 diff --git a/textures/airutils_painting.png b/textures/airutils_painting.png new file mode 100755 index 0000000000000000000000000000000000000000..0c02eafcf1636ea2eece0fb5cff784f3cde1f70a GIT binary patch literal 5541 zcmeHKdss|s7avDzN+E}?s%gq)_~t%S&D4}`YRaUh%M_wLGrMV;=3*|*6e8k;E=q`^ zP%f!Bha92sNf%{`k`I*#hhlOqm+0Fw?$3Ar@jT!8&pgkbz4v<8`mMEo@7n8q-wnY* zekMj%Mlcx6gzoRdgzl5H7hE6uw|g-Pz+ie;lEWh9OhAc{NX4=I1P~!ll7I+M#gB!- zR5vSFi*oPHG9LMB%5pu&o`yRBa!6k`!bmuWsTQy~%1hBs+dKpEP}PApXFq8Au`BX3 zSe@%0ZW*?+D@eGT;n{KhKq2qykXx&DjlNxCl^t%rK1C;p;ok^&}aY z@tUc6MYybT;avAyu$yxZwC9|je9B}Xg#pIgy|DR;%dy|h1BkU;LZumx5CZIVGIZ_( z*6&c)34#sbb9crTmn#WH)zz|>_g{!lkU{CCx!wQHci59noBSM}bn`NJ*5=L1_r;1E zO@^m$0I$b}oN;*ZYsfwuufT}q;@^k+3d<8t*e)NEg$m~U1^>0Vv9r3R5B{2(aUscZ zS_m)Ne%F4vukX|@N3SQ3g0@+K{qn-=0aGWT?WRe<%9xe^L7wzqe4dobpSF=|&~|%% z8f^A)cFcp!b?TFkj9DnJ;Y{2XLp6NP(4O7SqkpL={VO|+bFy4n=PvFu8pFXktMkWs z9c(RJ6QbCkpSZWE$g_ORTFrb}o&GV*_Dy#mc5btD%kEdYl-HIOcoj_8jI2w9dFva; zEvV0#X_G(J^tr|P1^Ut{2y^DvMv7=|Q>Vp5Y4Eu8OmeEa`tfVRzU6D+yfcQy`mYTC zC|7J=a5Tiz`-UZNm#k-;d9?I~wa4t1cfEer>hpTmi=W!3Suh)~_*h53#MfDbwrtoE z64#E|V;edjbw8unayK!8*V0%m!Cy3EtuUOvElj#@MOj7VTdVl;Ll5dNA6)Mxs$FNc zDE>|HSSI!Hoe8Vb9Bdyo+As(9-_o2K+|#f-xu={F;Ssf^<^68*&hlsUybzalm#tdt z|7A|Pxxu(It*dN*po!J^ z_2gxaZgAe#a>%p0C??3HUr#3PNr|b*-0n%UbUhrri+)Z2{`H#HY0rMU{#a2evWYsL zw55Tzt)48Ama(6uC7CSIB;=$#p|LK9L>|7nuu8waHJp!b71F5tr<0f$^i!L{Yp=X2 zsemtkb<3AmmerrjrU1LwR<2BwT6p57S4VVPBE9fnsgyzB&%jqYMr9}SXlbr ztx=}~dz$Mi2bklxx>f`n#Q0Y|I+E(nJ7{pmm6>}+cyVFWg~M4LgGTMmgswyQBfo?@ z&gr;#B(v|DnH}j+_m0~1xArD8@3?`7QaYbMi}H82NOo373u+C?J))b354i0d&~r2B zpHbp(a;(L$VCwn6;_{OG+n0BagOAjjCQq9kW_KC8-CRK*(t8(=nhAe(rJl>f&f7oq z{%yhJaOE$TB5nzkzDCSI(0${H6{wdc^(g}mB6;+y7p_apM$nnFDYLedIvidOtmJh4 zNfQCW2~YbXKeS6&s#W-*WTl2SYtGtnBZF)0>iX?iA$3h{b{MGJjY;Esdk53Ky+1cM zsHGLI%%S;Tm^gjhZxdS;y=td%O-#U8ulSzZ}2QB6o_p-}$QE z-Ujxy^B1F71}x7>Bd}#M8B?A!d35`fg>@=PSGIE4RyhdU69Vp>YBiSB?L<6OlHYtJ z{SGUwEgQZ!y}5-DI~`?m`rqT%!)cmcCOjWK+Wjyu%cuID-dQo9j zd}fuVX{iP3VRPW(1C+EWW=G+M$SEDco>QGBAMY=TY-~%p+Tiirm5_# zb3@=hsL?!ra+_H3Aq5ujUb;Q^_$E5E2wCx=#U_$5pUM#nF#uQ01~Dq31X_$>FgFjC z1mFljIf4!H_#zr|@bpb!J&Os{6qrM-3Z|(<;GH( zKJz|7KyNf;oLnxUVzEl45~Czy#8Ms>PoYq-I0BYHKtmd6S&~Q&sL&#rgBIc=h7Ty? zNcj>uUo1jsF#)z%A*Ugc&^Y3Ad_oC>@daKa`@{mo2UZ10uy_m(D->eCwUEhu6CsdK z0sXCoEDTz+u}n}VR!BLZZz3p?JA4bl<$ST1D5MFxbhsQWm;eePRT<H*h$a!70W<}0=AZ#59EC%|yAViDoNu7$BAFZzaX>8; z1dicDIBb9vO941UG})O=L6e-gcr?Jp18BU969pvT*(3s&{0+ncDIcmzAmQ7nv`}0K zis-_1CgaI2XfB=%q9OM2pA2)B=u z;?I{s8cEuU|5Wp#;IfZzA3uQvzHSJC(3LF};Cyr<0}?^5&JkqyaflNKhSSoZB(q{+y`REQsus7bPS3ysP68|U}42E&g zUSm|H9*L09Ku%})8r;{LY&-?k+kC_V5?RW9Bjw&=p?2R4({1NKE<(wd$04-aIacZf zbLdcJav?^+kP5(;=_pr$N#Jhe}mU^+QfLf!hJ$9?B|J zQ#-qJVH0IvSqlYEl&WF*H#QnfB07{FFUiD9l=T*A)R^fl6j`*nEJ&)p*J*_+4Y#+g z8{OQmX`z^!X7xo11NG2!mmTkfS#XNW=FrtUm_}8d ziXw;QF9or4T;~QD5;+;?3tuF?jCXDrSt~iX!4dhqRquq5qkhwX;Lgh(IA!)Ef1W@% zrcfr`J(H7R@ZnZc$fGA4@~)Pd&Dhx5Vr!>2))crs?E7B$Xt*S+uQ=~{Ms=Q7A?&2k z8aV3QA>j&ymgwIa99$yP8wpnnPL(kQ(fy8ZP5e=%yyg`9di{E4CO)EYhkecP=>AE^ oG&NYk*p7v!rV9mK!wf;D-NB-$oO=2JxOQ^szCk{fUNLL_3nzDcO#lD@ literal 0 HcmV?d00001