enu/src/game.nim

334 lines
11 KiB
Nim

import std / [monotimes, times, os, jsonutils, json, math]
import pkg / [godot, model_citizen, zippy / ziparchives]
import godotapi / [input, input_event, gd_os, node, scene_tree,
packed_scene, sprite, control, viewport, viewport_texture,
performance, label, theme, dynamic_font, resource_loader, main_loop,
project_settings, input_map, input_event_action, input_event_key, global_constants,
scroll_container]
import core, globals, controllers / [node_controllers, script_controllers], models / serializers
type
UserConfig = object
font_size: Option[int]
dock_icon_size: Option[float]
world: Option[string]
show_stats: Option[bool]
god_mode: Option[bool]
mega_pixels: Option[float]
start_full_screen: Option[bool]
semicolon_as_colon: Option[bool]
world_prefix: Option[string]
const auto_save_interval = 30.seconds
let state = GameState.active
let config = state.config
gdobj Game of Node:
var
reticle: Control
scaled_viewport: Viewport
triggered = false
saved_mouse_captured_state = false
stats: Label
last_tool = state.tool.value
saved_mouse_position: Vector2
scale_factor* = 0.0
rescale_at = get_mono_time()
save_at = get_mono_time() + auto_save_interval
node_controller: NodeController
script_controller: ScriptController
method process*(delta: float) =
inc state.frame_count
let time = get_mono_time()
if config.show_stats:
let fps = get_monitor(TIME_FPS)
var
total: times.Duration
highest: tuple[name: string, duration: times.Duration]
for name, dur in durations:
if dur > highest.duration:
highest = (name, dur)
total += dur
let vram = get_monitor(RENDER_VIDEO_MEM_USED)
self.stats.text = &"FPS: {fps}\nUser: {total}\n{highest.name}: {highest.duration}\nscale_factor: {self.scale_factor}\nvram: {vram}"
if time > self.rescale_at:
self.rescale_at = MonoTime.high
self.rescale()
if time > self.save_at:
self.save_at = time + auto_save_interval
save_world()
durations.clear()
proc rescale*() =
let vp = self.get_viewport().size
self.scale_factor = sqrt(config.mega_pixels * 1_000_000.0 / (vp.x * vp.y))
self.scaled_viewport.size = vp * self.scale_factor
method notification*(what: int) =
if what == main_loop.NOTIFICATION_WM_QUIT_REQUEST:
save_world()
self.get_tree().quit()
if what == main_loop.NOTIFICATION_WM_ABOUT:
alert(&"Enu {enu_version}\n\n© 2022 Scott Wadden", "Enu")
proc add_platform_input_actions =
let suffix = "." & host_os
for action in get_actions():
let action = action.as_string()
if suffix in action:
let name = action.replace(suffix, "")
if has_action(name):
erase_action(name)
add_action(name)
for event in get_action_list(action):
let event = event.as_object(InputEvent)
action_add_event(name, event)
erase_action(action)
proc load_user_config(): UserConfig =
let
work_dir = get_user_data_dir()
config_file = join_path(work_dir, "config.json")
if file_exists(config_file):
let opt = Joptions(allow_missing_keys: true, allow_extra_keys: true)
result.from_json(read_file(config_file).parse_json, opt)
proc save_user_config(config: UserConfig) =
let
work_dir = get_user_data_dir()
config_file = join_path(work_dir, "config.json")
write_file(config_file, config.to_json.pretty)
proc prepare_to_load_world() =
let work_dir = get_user_data_dir()
config.world_dir = join_path(work_dir, config.world)
config.data_dir = join_path(config.world_dir, "data")
config.script_dir = join_path(config.world_dir, "scripts")
if not file_exists(config.world_dir / "world.json"):
for file in walk_dir(config.lib_dir / "projects"):
if config.world.ends_with file.path.split_file.name:
file.path.extract_all(config.world_dir)
create_dir(state.config.data_dir)
create_dir(state.config.script_dir)
proc init* =
state.nodes.game = self
let
screen_scale = if host_os == "macos":
get_screen_scale(-1)
else:
get_screen_dpi(-1).float / 96.0
echo "Screen size: ", get_screen_size(-1), " scale ", screen_scale
var initial_user_config = self.load_user_config()
var uc = initial_user_config
assert not state.is_nil
assert not state.config.is_nil
state.config.font_size.value = uc.font_size ||= (14 * screen_scale).int
with state.config:
dock_icon_size = uc.dock_icon_size ||= 50 * screen_scale
world = uc.world ||= "default-1"
world_prefix = uc.world_prefix ||= "default"
show_stats = uc.show_stats ||= false
mega_pixels = uc.mega_pixels ||= 2.0
start_full_screen = uc.start_full_screen ||= true
semicolon_as_colon = uc.semicolon_as_colon ||= false
lib_dir = join_path(get_executable_path().parent_dir(), "..", "..", "..", "vmlib")
state.set_flag(God, uc.god_mode ||= false)
self.prepare_to_load_world()
set_window_fullscreen config.start_full_screen
if uc != initial_user_config:
self.save_user_config(uc)
self.add_platform_input_actions()
when defined(dist):
let exe_dir = parent_dir get_executable_path()
if host_os == "macosx":
config.lib_dir = join_path(exe_dir.parent_dir, "Resources", "vmlib")
elif host_os == "windows":
config.lib_dir = join_path(exe_dir, "vmlib")
elif host_os == "linux":
config.lib_dir = join_path(exe_dir.parent_dir, "lib", "vmlib")
self.node_controller = NodeController.init
self.script_controller = ScriptController.init
proc set_font_size(size: int) =
var user_config = self.load_user_config()
config.font_size.value = size
user_config.font_size = some(size)
self.save_user_config(user_config)
let
theme_holder = self.find_node("LeftPanel").as(Container)
theme = theme_holder.theme
font = theme.default_font.as(DynamicFont)
bold_font = theme.get_font("bold_font", "RichTextLabel")
.as(DynamicFont)
font.size = size
bold_font.size = size
theme_holder.theme = theme
method ready* =
state.nodes.data = state.nodes.game.find_node("Level").get_node("data")
assert not state.nodes.data.is_nil
self.scaled_viewport = self.get_node("ViewportContainer/Viewport") as Viewport
self.bind_signals(self.get_viewport(), "size_changed")
assert not self.scaled_viewport.is_nil
if config.mega_pixels >= 1.0:
self.scaled_viewport.get_texture.flags = FLAG_FILTER
self.scaled_viewport.fxaa = true
self.script_controller.load_player()
load_world(self.script_controller)
self.get_tree().auto_accept_quit = false
self.set_font_size config.font_size.value
self.reticle = self.find_node("Reticle").as(Control)
self.stats = self.find_node("stats").as(Label)
self.stats.visible = config.show_stats
state.flags.changes:
if MouseCaptured.added:
let center = self.get_viewport().get_visible_rect().size * 0.5
self.saved_mouse_position = self.get_viewport().get_mouse_position()
warp_mouse_position(center)
set_mouse_mode MOUSE_MODE_CAPTURED
elif MouseCaptured.removed:
set_mouse_mode MOUSE_MODE_VISIBLE
warp_mouse_position(self.saved_mouse_position)
if ReticleVisible.added:
self.reticle.visible = true
elif ReticleVisible.removed:
self.reticle.visible = false
state.push_flag MouseCaptured
proc update_action_index*(change: int) =
var index = int(state.tool.value) + change
if index < 0:
index = int Tools.high
elif index > int Tools.high:
index = int Tools.low
state.tool.value = Tools(index)
proc next_action*() =
self.update_action_index(1)
proc prev_action*() =
self.update_action_index(-1)
method on_size_changed() =
self.rescale_at = get_mono_time()
proc switch_world(diff: int) =
if diff != 0:
if config.world_prefix == "":
config.world_prefix = "tutorial"
var world = config.world
let prefix = config.world_prefix & "-"
world.remove_prefix(prefix)
var num = try:
world.parse_int
except ValueError:
1
num += diff
var user_config = self.load_user_config()
config.world = prefix & $num
user_config.world = some(config.world)
self.save_user_config(user_config)
save_world()
state.reloading = true
state.pop_flag Playing
state.units.clear
NodeController.reset_nodes
self.prepare_to_load_world()
load_world(self.script_controller)
state.reloading = false
method unhandled_input*(event: InputEvent) =
if EditorVisible in state.flags or ConsoleVisible in state.flags:
if event.is_action_pressed("zoom_in"):
self.set_font_size config.font_size.value + 1
elif event.is_action_pressed("zoom_out"):
self.set_font_size config.font_size.value - 1
else:
if event.is_action_pressed("next"):
self.next_action()
if event.is_action_pressed("previous"):
self.prev_action()
# NOTE: alt+enter isn't being picked up on windows if the editor is open. Needs investigation.
if event.is_action_pressed("toggle_fullscreen") or (host_os == "windows" and
CommandMode in state.flags and EditorVisible in state.flags and
event of InputEventKey and event.as(InputEventKey).scancode == KEY_ENTER):
set_window_fullscreen not is_window_fullscreen()
elif event.is_action_pressed("next_world"):
self.switch_world(+1)
elif event.is_action_pressed("prev_world"):
self.switch_world(-1)
elif event.is_action_pressed("command_mode"):
state.push_flag CommandMode
elif event.is_action_released("command_mode"):
state.pop_flag CommandMode
elif event.is_action_pressed("save_and_reload"):
self.switch_world(0)
self.get_tree().set_input_as_handled()
state.pop_flag Playing
elif event.is_action_pressed("pause"):
state.paused = not state.paused
elif event.is_action_pressed("clear_console"):
state.console.log.clear()
elif event.is_action_pressed("toggle_console"):
state.set_flag ConsoleVisible, ConsoleVisible notin state.flags
elif event.is_action_pressed("quit"):
if host_os != "macosx":
save_world()
self.get_tree().quit()
elif EditorVisible notin state.flags:
if event.is_action_pressed("toggle_mouse_captured"):
state.set_flag MouseCaptured, MouseCaptured notin state.flags
self.get_tree().set_input_as_handled()
if event.is_action_pressed("toggle_code_mode"):
if state.tool.value != CodeMode:
self.last_tool = state.tool.value
state.tool.value = CodeMode
else:
state.tool.value = self.last_tool
elif event.is_action_pressed("mode_1"):
state.tool.value = CodeMode
elif event.is_action_pressed("mode_2"):
state.tool.value = BlueBlock
elif event.is_action_pressed("mode_3"):
state.tool.value = RedBlock
elif event.is_action_pressed("mode_4"):
state.tool.value = GreenBlock
elif event.is_action_pressed("mode_5"):
state.tool.value = BlackBlock
elif event.is_action_pressed("mode_6"):
state.tool.value = WhiteBlock
elif event.is_action_pressed("mode_7"):
state.tool.value = BrownBlock
elif event.is_action_pressed("mode_8"):
state.tool.value = PlaceBot
proc get_game*(): Game = Game(state.nodes.game)