diff --git a/scenes/player.tscn b/scenes/player.tscn index 1e98353..64f0831 100644 --- a/scenes/player.tscn +++ b/scenes/player.tscn @@ -62,6 +62,10 @@ advance_mode = 2 advance_mode = 2 advance_expression = "is_aiming()" +[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_qfm1y"] +advance_mode = 2 +advance_expression = "not is_aiming()" + [sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_oprun"] advance_mode = 2 advance_expression = "is_firing()" @@ -70,18 +74,14 @@ advance_expression = "is_firing()" switch_mode = 2 advance_mode = 2 -[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_qfm1y"] -advance_mode = 2 -advance_expression = "not is_aiming()" - [sub_resource type="AnimationNodeStateMachine" id="AnimationNodeStateMachine_3v2ag"] states/Aim/node = SubResource("AnimationNodeAnimation_d2wvv") states/Aim/position = Vector2(551, 100) states/Fire/node = SubResource("AnimationNodeAnimation_3v2ag") -states/Fire/position = Vector2(687, 100) +states/Fire/position = Vector2(730, 100) states/Movement/node = SubResource("AnimationNodeBlendTree_d2wvv") states/Movement/position = Vector2(403, 100) -transitions = ["Start", "Movement", SubResource("AnimationNodeStateMachineTransition_jej6c"), "Movement", "Aim", SubResource("AnimationNodeStateMachineTransition_f1ej7"), "Aim", "Fire", SubResource("AnimationNodeStateMachineTransition_oprun"), "Fire", "Aim", SubResource("AnimationNodeStateMachineTransition_a8ls1"), "Aim", "Movement", SubResource("AnimationNodeStateMachineTransition_qfm1y")] +transitions = ["Start", "Movement", SubResource("AnimationNodeStateMachineTransition_jej6c"), "Movement", "Aim", SubResource("AnimationNodeStateMachineTransition_f1ej7"), "Aim", "Movement", SubResource("AnimationNodeStateMachineTransition_qfm1y"), "Aim", "Fire", SubResource("AnimationNodeStateMachineTransition_oprun"), "Fire", "Aim", SubResource("AnimationNodeStateMachineTransition_a8ls1")] [sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_sk752"] animation = &"Idle" @@ -162,7 +162,7 @@ tree_root = SubResource("AnimationNodeStateMachine_5lvsk") advance_expression_base_node = NodePath("..") anim_player = NodePath("../Mesh/AnimationPlayer") parameters/Handgun/Movement/Blend2/blend_amount = 1.0 -parameters/Handgun/Movement/BlendSpace1D/blend_position = 0.229277 +parameters/Handgun/Movement/BlendSpace1D/blend_position = 0.5 parameters/Unarmed/blend_position = 0.872134 script = ExtResource("3_26yay") move_speed_expression = "move_speed()" diff --git a/src/async_resource_handler.ts b/src/async_resource_handler.ts new file mode 100644 index 0000000..569b6c2 --- /dev/null +++ b/src/async_resource_handler.ts @@ -0,0 +1,73 @@ +import { GArray, GError, Node, Resource, ResourceLoader, ResourceSaver } from 'godot' +import { GodotError } from './godot_error' + +export class ResourceLoadError extends Error { } + +export default class AsyncResourceHandler extends Node { + private static _instance: AsyncResourceHandler + static readonly SaveFlags = ResourceSaver.SaverFlags + static readonly CacheMode = ResourceLoader.CacheMode + + static get instance() { + return this._instance + } + + _ready(): void { + AsyncResourceHandler._instance = this + } + + static save(path: string, data: T, flags: ResourceSaver.SaverFlags): Promise { + return this._instance.save(path, data, flags) + } + + save(path: string, data: T, flags: ResourceSaver.SaverFlags): Promise { + return new Promise((resolve, reject) => { + const err = ResourceSaver.save(data, path, flags) + + if (err === GError.OK) { + resolve() + } else { + reject(err) + } + }) + } + + static load(path: string, hint_string: string = '', use_sub_threads?: boolean, cache_mode?: ResourceLoader.CacheMode): Promise { + return this._instance.load(path, hint_string, use_sub_threads, cache_mode) + } + + load(path: string, hint_string: string = '', use_sub_threads?: boolean, cache_mode?: ResourceLoader.CacheMode): Promise { + return new Promise(async (resolve, reject) => { + const err = ResourceLoader.load_threaded_request(path, hint_string, use_sub_threads, cache_mode) + + if (err != GError.OK) { + return reject(new GodotError(err)) + } + + const get_status = () => ResourceLoader.load_threaded_get_status(path) + + let status = get_status() + while (status === ResourceLoader.ThreadLoadStatus.THREAD_LOAD_IN_PROGRESS) { + await this.get_tree().process_frame.as_promise() + status = get_status() + } + + if (status === ResourceLoader.ThreadLoadStatus.THREAD_LOAD_LOADED) { + return resolve(ResourceLoader.load_threaded_get(path) as T) + } else { + return reject(new ResourceLoadError()) + } + }) + } + + static get_progress(path: string): number { + return this._instance.get_progress(path) + } + + get_progress(path: string): number { + const arr = new GArray() + ResourceLoader.load_threaded_get_status(path, arr) + return arr.get_indexed(0) + } +} + diff --git a/src/async_resource_handler.ts.uid b/src/async_resource_handler.ts.uid new file mode 100644 index 0000000..a387ea8 --- /dev/null +++ b/src/async_resource_handler.ts.uid @@ -0,0 +1 @@ +uid://byri2tpbtahpd diff --git a/src/async_resource_loader.ts b/src/async_resource_loader.ts index 1b521af..329d7db 100644 --- a/src/async_resource_loader.ts +++ b/src/async_resource_loader.ts @@ -1,22 +1,39 @@ -import { GError, Node, Resource, ResourceLoader } from 'godot' +import { GArray, GError, Node, Resource, ResourceLoader, ResourceSaver } from 'godot' +import { GodotError } from './godot_error' -export default class AsyncResourceLoader extends Node { - private static _instance: AsyncResourceLoader +export class ResourceLoadError extends Error { } + +export default class AsyncResourceHandler extends Node { + private static _instance: AsyncResourceHandler + static readonly SaveFlags = ResourceSaver.SaverFlags + static readonly CacheMode = ResourceLoader.CacheMode static get instance() { return this._instance } _ready(): void { - AsyncResourceLoader._instance = this + AsyncResourceHandler._instance = this } - load(path: string, hint_string: string = ''): Promise { + save(path: string, data: T, flags: ResourceSaver.SaverFlags): Promise { + return new Promise((resolve, reject) => { + const err = ResourceSaver.save(data, path, flags) + + if (err === GError.OK) { + resolve() + } else { + reject(err) + } + }) + } + + load(path: string, hint_string: string = '', use_sub_threads?: boolean, cache_mode?: ResourceLoader.CacheMode): Promise { return new Promise(async (resolve, reject) => { - const err = ResourceLoader.load_threaded_request(path, hint_string) + const err = ResourceLoader.load_threaded_request(path, hint_string, use_sub_threads, cache_mode) if (err != GError.OK) { - return reject(new Error(`failed to load ${path}`)) + return reject(new GodotError(err)) } const get_status = () => ResourceLoader.load_threaded_get_status(path) @@ -27,12 +44,18 @@ export default class AsyncResourceLoader extends Node { status = get_status() } - if (get_status() === ResourceLoader.ThreadLoadStatus.THREAD_LOAD_LOADED) { + if (status === ResourceLoader.ThreadLoadStatus.THREAD_LOAD_LOADED) { return resolve(ResourceLoader.load_threaded_get(path) as T) } else { - return reject(new Error(`failed to load ${path}`)) + return reject(new ResourceLoadError()) } }) } + + get_progress(path: string): number { + const arr = new GArray() + ResourceLoader.load_threaded_get_status(path, arr) + return arr.get_indexed(0) + } } diff --git a/src/equipped_weapon.ts b/src/equipped_weapon.ts index 3804c73..4c459b9 100644 --- a/src/equipped_weapon.ts +++ b/src/equipped_weapon.ts @@ -2,10 +2,10 @@ import { Color, GArray, Node, Node3D, PackedScene, PhysicsBody3D, PhysicsRayQuer import { export_file, signal } from 'godot.annotations' import { export_node, onready } from './annotations' import Weapon from './weapon' -import AsyncResourceLoader from './async_resource_loader' +import AsyncResourceHandler from './async_resource_handler' import { forward } from './vec' import DebugDraw from './debug_draw' -import { find_in_anscestors, find_in_descendents, implements_interface } from './node' +import { find_in_anscestors, find_in_descendents, has_method } from './node' import { Damageable } from './health' const { MULTIPLY: mul, ADD: add } = Vector3 @@ -36,7 +36,7 @@ export default class EquippedWeapon extends Node3D { _ready(): void { if (this.starting_weapon != null) { - AsyncResourceLoader.instance.load(this.starting_weapon, 'Weapon').then(weapon => this.equip(weapon)) + AsyncResourceHandler.instance.load(this.starting_weapon, 'Weapon').then(weapon => this.equip(weapon)) } } @@ -61,7 +61,7 @@ export default class EquippedWeapon extends Node3D { } equip(weapon: Weapon) { - AsyncResourceLoader.instance.load(weapon.scene, 'PackedScene') + AsyncResourceHandler.instance.load(weapon.scene, 'PackedScene') .then(scene => this._parent_scene_to_transform(scene)) if (this._has_equipped_weapon) { @@ -86,7 +86,7 @@ export default class EquippedWeapon extends Node3D { if (!result.is_empty()) { DebugDraw.draw_ray(query, Color.DARK_RED) const root = find_in_anscestors(result.get('collider'), n => n instanceof PhysicsBody3D) - const health = find_in_descendents(root, n => implements_interface(['apply_damage'], n)) + const health = find_in_descendents(root, n => has_method('apply_damage', n)) if (health != null) { health.apply_damage(this, this._equipped_weapon.damage) } diff --git a/src/godot_error.ts b/src/godot_error.ts new file mode 100644 index 0000000..d11fe1e --- /dev/null +++ b/src/godot_error.ts @@ -0,0 +1,12 @@ +import { GError } from "godot"; + +export class GodotError extends Error { + error: GError + + constructor(err: GError, message?: string) { + super(message || `Godot error (${err})`) + this.error = err + } + +} + diff --git a/src/godot_error.ts.uid b/src/godot_error.ts.uid new file mode 100644 index 0000000..1c8d671 --- /dev/null +++ b/src/godot_error.ts.uid @@ -0,0 +1 @@ +uid://o3gcj3wh78yt diff --git a/src/node.ts b/src/node.ts index c6409f4..aa500bb 100644 --- a/src/node.ts +++ b/src/node.ts @@ -11,8 +11,14 @@ export function queue_free_children(node: T) { } } +type RequiredField = Omit & Required> + +export function has_method(name: StringName, obj: Object): boolean { + return obj.has_method(name) +} + export function implements_interface(method_names: StringName[], obj: Object): obj is Object & T { - return method_names.every(name => obj.has_method(name)) + return method_names.every(name => has_method(name, obj)) } export function find_in_anscestors(root: T | null | undefined, predicate: Predicate, height: number = Infinity): T | null | undefined { diff --git a/src/node.ts.uid b/src/node.ts.uid new file mode 100644 index 0000000..6b0b660 --- /dev/null +++ b/src/node.ts.uid @@ -0,0 +1 @@ +uid://d0npi365fttf6 diff --git a/src/save_serializer.ts b/src/save_serializer.ts new file mode 100644 index 0000000..4608478 --- /dev/null +++ b/src/save_serializer.ts @@ -0,0 +1,87 @@ +import { GDictionary, Node, PackedScene, Resource, ResourceLoader, ResourceSaver } from 'godot' +import AsyncResourceHandler from './async_resource_handler' +import { has_method } from './node' + +interface Serializable { + on_save_game(save_data: GameState): void + on_before_load_game?(): void + on_load_game(state: T): void +} + +interface SaveState extends Resource { + scene: string +} + +class PlayerState extends Resource implements SaveState { + condition: undefined = undefined + inventory: undefined = undefined + scene: string = '' +} + +class ProgressState extends Resource { + variables?: GDictionary +} + +export class GameState extends Resource { + player?: PlayerState + progress?: ProgressState + world?: SaveState[] +} + +const StateMethod = { + Save: 'on_save_game', + BeforeLoad: 'on_before_load_game', + Load: 'on_load_game' +} as const + +type StateMethod = typeof StateMethod[keyof typeof StateMethod] + +export default class SaveSerializer extends Node { + private static _save_group: string = 'save' + + private static _instance: SaveSerializer + + _ready() { + SaveSerializer._instance = this + } + + static save() { + this._instance.save() + } + + private _announce(method: StateMethod, ...args: any) { + this.get_tree().call_group( + SaveSerializer._save_group, + method, + ...args + ) + } + + save() { + const game_state: GameState = new GameState() + + this._announce(StateMethod.Save, game_state) + + ResourceSaver.save(game_state, 'user://save1.tres') + } + + private async _restore_node(state: T, parent: Node) { + const scene: PackedScene = await AsyncResourceHandler.load(state.scene, 'PackedScene') + const node = scene.instantiate() + parent.add_child(node) + + if (has_method(StateMethod.Load, node)) { + // TODO: type check for state method + } + + } + + async load() { + const loaded_state: GameState = await AsyncResourceHandler.load('user://save1.tres') + this._announce(StateMethod.BeforeLoad) + // TODO: pass world root as 2nd param + const node_promises = loaded_state.world?.map(state => this._restore_node(state, this)) ?? [] + return Promise.all(node_promises) + } +} + diff --git a/src/save_serializer.ts.uid b/src/save_serializer.ts.uid new file mode 100644 index 0000000..3424e91 --- /dev/null +++ b/src/save_serializer.ts.uid @@ -0,0 +1 @@ +uid://xv4ix5esb3ij