--!strict
-- src/Client/Systems/FlightSystem.lua

local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")

type FlightState = {
    Velocity: Vector3,
    Acceleration: Vector3,

    Wind: {
        Direction: Vector3,
        Strength: number,
    },

    Altitude: {
        Target: number,
        Smoothness: number,
    },

    Thrust: {
        Power: number,
        MaxSpeed: number,
    },

    Drag: number,
}

type InputState = {
    Thrust: number, -- 0.0〜1.0
}

local FlightSystem = {}
FlightSystem.__index = FlightSystem

-- ユーティリティ:ベクトルを最大長でクランプ
local function clampMagnitude(v: Vector3, maxLen: number): Vector3
    local mag = v.Magnitude
    if mag > maxLen then
        return v.Unit * maxLen
    end
    return v
end

-- コンストラクタ
function FlightSystem.new(player: Player)
    local self = setmetatable({}, FlightSystem)

    self.Player = player
    self.Character = player.Character or player.CharacterAdded:Wait()
    self.RootPart = self.Character:WaitForChild("HumanoidRootPart") :: BasePart
    self.Humanoid = self.Character:WaitForChild("Humanoid") :: Humanoid

    -- 飛行用に Humanoid の制御を弱める(必要に応じて調整)
    self.Humanoid.PlatformStand = true

    self.State = {
        Velocity = Vector3.new(),
        Acceleration = Vector3.new(),

        Wind = {
            Direction = Vector3.new(1, 0, 0),
            Strength = 0.0,
        },

        Altitude = {
            Target = self.RootPart.Position.Y + 20,
            Smoothness = 0.1,
        },

        Thrust = {
            Power = 80,
            MaxSpeed = 140,
        },

        Drag = 0.05,
    } :: FlightState

    self.Input = {
        Thrust = 0.0,
    } :: InputState

    self._connInputBegan = UserInputService.InputBegan:Connect(function(input, processed)
        if processed then
            return
        end
        self:_onInputBegan(input)
    end)

    self._connInputEnded = UserInputService.InputEnded:Connect(function(input, processed)
        if processed then
            return
        end
        self:_onInputEnded(input)
    end)

    self._connStepped = RunService.RenderStepped:Connect(function(dt)
        self:Update(dt)
    end)

    return self
end

-- 入力処理(例:Space で推力オン)
function FlightSystem:_onInputBegan(input: InputObject)
    if input.KeyCode == Enum.KeyCode.Space then
        self.Input.Thrust = 1.0
    end
end

function FlightSystem:_onInputEnded(input: InputObject)
    if input.KeyCode == Enum.KeyCode.Space then
        self.Input.Thrust = 0.0
    end
end

-- 外部 API:風向き設定
function FlightSystem:SetWindDirection(dir: Vector3)
    if dir.Magnitude > 0 then
        self.State.Wind.Direction = dir.Unit
    else
        self.State.Wind.Direction = Vector3.new(0, 0, 0)
    end
end

-- 外部 API:風の強さ設定
function FlightSystem:SetWindStrength(strength: number)
    self.State.Wind.Strength = strength
end

-- 外部 API:目標高度設定
function FlightSystem:SetAltitudeTarget(height: number)
    self.State.Altitude.Target = height
end

-- 外部 API:推力の強さ(0〜1)を直接指定したい場合
function FlightSystem:ApplyThrust(amount: number)
    self.Input.Thrust = math.clamp(amount, 0, 1)
end

-- メイン更新処理
function FlightSystem:Update(dt: number)
    local state = self.State
    local root = self.RootPart
    if not root or not root.Parent then
        return
    end

    state.Acceleration = Vector3.new()

    -- カメラの向きを取得(前方方向を飛行方向とする)
    local camera = workspace.CurrentCamera
    if not camera then
        return
    end

    local look = camera.CFrame.LookVector

    -- 1. 推力(Thrust)
    if self.Input.Thrust > 0 then
        local thrustForce = look * state.Thrust.Power * self.Input.Thrust
        state.Acceleration += thrustForce
    end

    -- 2. 滑空(Glide)=重力の一部だけ適用
    local glideForce = Vector3.new(0, -workspace.Gravity * 0.3, 0)
    state.Acceleration += glideForce

    -- 3. 風の影響(WindInfluence)
    local windForce = state.Wind.Direction * state.Wind.Strength
    state.Acceleration += windForce

    -- 4. 高度制御(AltitudeControl)
    local currentY = root.Position.Y
    local diff = state.Altitude.Target - currentY
    local altitudeForce = Vector3.new(0, diff * state.Altitude.Smoothness, 0)
    state.Acceleration += altitudeForce

    -- 5. 速度更新
    state.Velocity += state.Acceleration * dt

    -- Drag:Lerp で徐々に減速
    state.Velocity = state.Velocity:Lerp(Vector3.new(), state.Drag)

    -- 最大速度制限
    state.Velocity = clampMagnitude(state.Velocity, state.Thrust.MaxSpeed)

    -- 6. 位置更新
    -- CFrame にベクトルを足すとその方向に平行移動
    root.CFrame = root.CFrame + state.Velocity * dt
end

-- クリーンアップ
function FlightSystem:Destroy()
    if self._connInputBegan then
        self._connInputBegan:Disconnect()
    end
    if self._connInputEnded then
        self._connInputEnded:Disconnect()
    end
    if self._connStepped then
        self._connStepped:Disconnect()
    end

    if self.Humanoid then
        self.Humanoid.PlatformStand = false
    end
end

return FlightSystem

使い方(Client 側)

-- 例: src/Client/Controllers/PlayerController.lua

local Players = game:GetService("Players")
local FlightSystem = require(path.to.FlightSystem)

local player = Players.LocalPlayer
local flight = FlightSystem.new(player)

-- 風や高度を外部から制御したいとき
flight:SetWindDirection(Vector3.new(1, 0, 0))
flight:SetWindStrength(0.8)
flight:SetAltitudeTarget(80)

-- 不要になったら
-- flight:Destroy()

このまま ModuleScript として貼れば、
「Space キーで前方に推力をかけつつ、
滑空+風+高度制御を併用した空中戦の物理レイヤー」が動きます。

次にやるなら:

  • 敵(巨大蚊)の FlightSystem 版(AI 用)
  • BowController と組み合わせた空中射撃
  • FlightCarrierService との連携(高度ターゲット=空母周りに固定)

どこを続きで詰めたいか教えてください。