flatland3-gfx 0.2.8

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_game_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};
use crate::window_prefs::{apply_saved_position, capture_window_geometry, save_window_geometry};

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

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

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

    /// Skip automatic asset sync before connecting
    #[arg(long)]
    pub skip_asset_sync: 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();
    let mut window_position_applied = false;
    let mut last_window_save = Instant::now();
    let mut last_saved_geometry = capture_window_geometry();

    'play: loop {
        let snapshot = net.snapshot();
        let state = &snapshot.game;
        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 renderer.show_connection_settings {
                renderer.show_connection_settings = false;
            } else 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();
            }
        }

        if is_key_pressed(KeyCode::F2) {
            renderer.show_connection_settings = !renderer.show_connection_settings;
        }

        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 (facing_f, facing_s) = if let Some((f, s)) = snapshot.presentation.auto_nav_facing_axes
        {
            (f, s)
        } else {
            let (lf, ls) = local.movement.last_move_axes();
            let (cf, cs) = local.movement.current().components();
            if cf.abs() > f32::EPSILON || cs.abs() > f32::EPSILON {
                (cf, cs)
            } else {
                (lf, 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();

        if local.was_harvesting && !state.harvest_in_progress {
            renderer.trigger_harvest_flash();
        }
        local.was_harvesting = state.harvest_in_progress;
        renderer
            .maybe_reload_sprites_for_publish_rev(state.content_rev, state.publish_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));
        }

        if !window_position_applied {
            apply_saved_position();
            window_position_applied = true;
        }
        let geometry = capture_window_geometry();
        if geometry.width != last_saved_geometry.width
            || geometry.height != last_saved_geometry.height
            || geometry.x != last_saved_geometry.x
            || geometry.y != last_saved_geometry.y
        {
            last_saved_geometry = geometry;
            if last_window_save.elapsed() >= std::time::Duration::from_millis(500) {
                save_window_geometry(last_saved_geometry.clone());
                last_window_save = Instant::now();
            }
        }

        next_frame().await;
    }

    save_window_geometry(capture_window_geometry());
    Ok(())
}