flatland3-gfx 0.2.4

Flatland3 Macroquad + egui graphical play client
//! Macroquad + egui play loop for Flatland3.

use std::net::SocketAddr;
use std::time::Instant;

use clap::Parser;
use flatland_client_lib::{default_server_addr, load_client_settings};
use flatland_client_ui::{
    ActiveOverlay, InputAction, UiKeyCode, UiKeyEvent, UiKeyEventKind, UiKeyModifiers,
    UiMouseButton, UiPointerEvent,
};
use flatland_gfx_engine::{
    blocks_map_zoom, is_map_zoom_key, menu_blocking, screen_to_world, zoom_from_input,
    GfxInputResult, GfxRenderer,
};
use flatland_play_loop::{
    build_play_hud, movement_interval_elapsed, should_dispatch_to_net, PlayHudInput, PlayLocalState,
};
use flatland_protocol::ChatChannel;
use macroquad::prelude::*;

use crate::network::{GfxNet, NetCmd};

#[derive(Debug, Parser)]
pub struct GfxPlayArgs {
    #[arg(short, long)]
    pub name: Option<String>,

    #[arg(long, default_value_t = default_server_addr())]
    pub server: SocketAddr,

    #[arg(long)]
    pub no_reconnect: bool,
}

pub async fn run(args: GfxPlayArgs) -> anyhow::Result<()> {
    let net = GfxNet::spawn(args)?;
    let keys = load_client_settings().keys;
    let mut local = PlayLocalState::new(keys);
    let mut renderer = GfxRenderer::init().await;
    renderer.hud_view = local.hud_view;
    let mut movement_sent_at = Instant::now();

    'play: loop {
        let state = net.snapshot();
        let input = renderer.poll_input(local.chat.active);
        let block_zoom = blocks_map_zoom(&state, local.chat.active, &local.map_target);
        let zoom_result = if block_zoom {
            GfxInputResult::default()
        } else {
            zoom_from_input(&input, renderer.map_zoom)
        };
        renderer.apply_input_result(zoom_result);

        if is_key_pressed(KeyCode::Escape) {
            if state.show_npc_chat || state.show_npc_verb_menu || state.show_shop_menu {
                net.send(NetCmd::Esc);
            } else if local.map_target.active {
                local.clear_nav();
                net.send(NetCmd::CancelAutoNav);
            } else if local.chat.active {
                local.chat.close();
            } else if renderer.show_help {
                renderer.show_help = false;
            } else if state.show_loadout_menu || state.show_rotation_editor {
                let overlay = if state.show_loadout_menu {
                    ActiveOverlay::Loadout
                } else {
                    ActiveOverlay::RotationEditor(state.rotation_editor.mode)
                };
                let esc = UiKeyEvent {
                    kind: UiKeyEventKind::Press,
                    code: UiKeyCode::Esc,
                    modifiers: UiKeyModifiers::default(),
                };
                match local
                    .movement
                    .apply_ui_key(esc, overlay, false)
                {
                    InputAction::Quit => {}
                    action => net.send(NetCmd::Action(action)),
                }
                local.movement.reset();
            } else {
                net.send(NetCmd::Esc);
                local.clear_nav();
            }
        }

        for key in input.keys {
            if local.chat.active {
                if let Some(text) = local.chat.handle_key(key) {
                    let channel = if local.chat.whisper {
                        ChatChannel::WhisperStone
                    } else {
                        ChatChannel::Nearby
                    };
                    net.send(NetCmd::Say(channel, text));
                }
                continue;
            }

            if menu_blocking(&state) {
                net.send(NetCmd::MenuKey(key));
                continue;
            }

            if key.kind == UiKeyEventKind::Press && matches!(key.code, UiKeyCode::Char(';')) {
                renderer.show_use_radius = !renderer.show_use_radius;
                continue;
            }

            if is_map_zoom_key(&key) && !state.show_rotation_editor {
                continue;
            }

            let overlay = if state.show_loadout_menu {
                ActiveOverlay::Loadout
            } else if state.show_rotation_editor {
                ActiveOverlay::RotationEditor(state.rotation_editor.mode)
            } else {
                ActiveOverlay::None
            };
            match local
                .movement
                .apply_ui_key(key, overlay, local.map_target.active)
            {
                InputAction::Quit => break 'play,
                InputAction::ToggleHelp => renderer.show_help = !renderer.show_help,
                InputAction::CycleHudView => {
                    local.cycle_hud_view();
                    renderer.hud_view = local.hud_view;
                }
                InputAction::ToggleMapTarget => {
                    if local.map_target.active {
                        local.map_target.deactivate();
                    } else {
                        let (px, py, _) = state.player_position_with_z();
                        local.map_target.activate_at(px, py);
                    }
                }
                InputAction::ConfirmMapTarget => {
                    if local.map_target.active {
                        local.auto_nav_goal =
                            Some((local.map_target.cursor_x, local.map_target.cursor_y));
                        net.send(NetCmd::AutoNavTo {
                            x: local.map_target.cursor_x,
                            y: local.map_target.cursor_y,
                        });
                        local.map_target.deactivate();
                    }
                }
                InputAction::CancelMapTarget | InputAction::CancelAutoNav => {
                    local.clear_nav();
                    net.send(NetCmd::CancelAutoNav);
                }
                InputAction::StopMovement => {
                    local.clear_nav();
                    net.send(NetCmd::StopMovement);
                }
                InputAction::StartChat { whisper } => local.chat.open(whisper),
                InputAction::ToggleSprintMode => local.movement.toggle_sprint_mode(),
                InputAction::MapTargetNudge { dx, dy } => {
                    local.map_target.nudge(
                        dx,
                        dy,
                        state.world_width_m,
                        state.world_height_m,
                    );
                }
                other => {
                    if should_dispatch_to_net(other) {
                        net.send(NetCmd::Action(other));
                    }
                }
            }
        }

        for ptr in input.pointers {
            match ptr {
                UiPointerEvent::Move { x, y } => {
                    local.mouse_hover = screen_to_world(
                        &renderer.map_grid,
                        x,
                        y,
                        &state,
                        &renderer.world_rect,
                        &renderer.camera,
                    );
                }
                UiPointerEvent::Click { x, y, button } => {
                    if let Some((wx, wy)) = screen_to_world(
                        &renderer.map_grid,
                        x,
                        y,
                        &state,
                        &renderer.world_rect,
                        &renderer.camera,
                    ) {
                        match button {
                            UiMouseButton::Left => {
                                local.auto_nav_goal = Some((wx, wy));
                                net.send(NetCmd::AutoNavTo { x: wx, y: wy });
                                local.map_target.deactivate();
                            }
                            UiMouseButton::Right => {
                                local.clear_nav();
                                net.send(NetCmd::CancelAutoNav);
                            }
                            UiMouseButton::Middle => {}
                        }
                    }
                }
                UiPointerEvent::Scroll { .. } => {}
            }
        }

        let movement_blocked = menu_blocking(&state)
            || state.show_loadout_menu
            || state.show_rotation_editor
            || local.chat.active
            || local.map_target.active;

        let now = Instant::now();
        if !movement_blocked && movement_interval_elapsed(movement_sent_at, now) {
            movement_sent_at = now;
            let (f, s) = local.movement.current().components();
            net.send(NetCmd::Movement {
                forward: f,
                strafe: s,
                vertical: local.movement.vertical_axis(),
                sprint: local.movement.sprinting(),
            });
        }

        local
            .movement
            .expire_idle(local.movement.idle_timeout());

        let (px, py) = state.player_position();
        let (lf, ls) = local.movement.last_move_axes();
        let (cf, cs) = local.movement.current().components();
        let facing_f = if cf.abs() > f32::EPSILON || cs.abs() > f32::EPSILON {
            cf
        } else {
            lf
        };
        let facing_s = if cf.abs() > f32::EPSILON || cs.abs() > f32::EPSILON {
            cs
        } else {
            ls
        };
        let now_ms = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map(|d| d.as_millis() as u64)
            .unwrap_or(0);
        renderer.update_camera(px, py, facing_f, facing_s, now_ms);

        let movement_input = local.movement.current();
        let hud = build_play_hud(PlayHudInput {
            movement_label: movement_input.label(),
            sprint_mode: local.movement.sprint_mode(),
            map_target: &local.map_target,
            auto_nav_goal: local.auto_nav_goal,
            mouse_hover: local.mouse_hover,
            view_mode: local.hud_view,
        });
        let chat_line = local.chat.prompt_line();

        let state = net.snapshot();
        if local.was_harvesting && !state.harvest_in_progress {
            renderer.trigger_harvest_flash();
        }
        local.was_harvesting = state.harvest_in_progress;
        renderer
            .maybe_reload_sprites_for_content_rev(state.content_rev)
            .await;
        renderer.tick_fx(get_frame_time());
        renderer.draw_frame(&state, &hud, &chat_line);
        if let Some(idx) = renderer.take_menu_actions().keychain_select {
            net.send(NetCmd::KeychainSelect(idx));
        }
        next_frame().await;
    }

    Ok(())
}