Refine sprint and landing dust effects
This commit is contained in:
parent
c239ca6bd5
commit
7d38bc962b
5 changed files with 350 additions and 31 deletions
|
|
@ -34,6 +34,7 @@ const FishingRodAttachmentScene = preload(
|
|||
const HeldItemAttachmentScene = preload(
|
||||
"res://player/held_item_attachment.tscn"
|
||||
)
|
||||
const SprintDustTrailType = preload("res://player/sprint_dust_trail.gd")
|
||||
|
||||
signal retract_visual_finished
|
||||
|
||||
|
|
@ -90,6 +91,7 @@ const BLINK_DURATION_SECONDS: float = 0.11
|
|||
const CHARACTER_CALL_MOUTH_ID: String = "open_ah"
|
||||
const CHARACTER_CALL_MOUTH_DURATION_SECONDS: float = 0.16
|
||||
const BASE_REEL_SPEED: float = 0.16
|
||||
const LANDING_DUST_MIN_FALL_SPEED: float = 2.5
|
||||
# The target Android handheld exposes its physical right trigger through
|
||||
# Godot's left-trigger axis. Keep the role named here so the platform mapping
|
||||
# remains isolated from camera behavior.
|
||||
|
|
@ -424,8 +426,11 @@ var _fishing_rod: Node3D
|
|||
var _fishing_rod_tip: Marker3D
|
||||
var _active_item_is_rod: bool = false
|
||||
var _controller_mapping_manager: ControllerMappingManagerType
|
||||
var _sprint_dust_landing_ready: bool = false
|
||||
var _sprint_dust_airborne: bool = false
|
||||
var _sprint_dust_fall_speed: float = 0.0
|
||||
|
||||
@onready var _sprint_dust: CPUParticles3D = %SprintDust
|
||||
@onready var _sprint_dust: SprintDustTrailType = %SprintDust
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -608,7 +613,7 @@ func _physics_process(delta: float) -> void:
|
|||
func _process(delta: float) -> void:
|
||||
_update_blink(delta)
|
||||
_update_character_animation()
|
||||
_update_sprint_dust()
|
||||
_update_sprint_dust(delta)
|
||||
# The hand bone supplies the attachment position, but its animated wrist
|
||||
# rotation should not turn the flat fish/catch artwork edge-on. Keep each
|
||||
# display aligned with the character's facing while it follows the hand.
|
||||
|
|
@ -694,24 +699,54 @@ func _schedule_next_blink() -> void:
|
|||
)
|
||||
|
||||
|
||||
func _update_sprint_dust() -> void:
|
||||
func _update_sprint_dust(delta: float) -> void:
|
||||
if _sprint_dust == null:
|
||||
return
|
||||
var horizontal_speed_squared: float = (
|
||||
velocity.x * velocity.x + velocity.z * velocity.z
|
||||
)
|
||||
var horizontal_velocity := Vector3(velocity.x, 0.0, velocity.z)
|
||||
var grounded: bool = is_on_floor()
|
||||
if _water_recovery_active:
|
||||
_sprint_dust_airborne = false
|
||||
_sprint_dust_fall_speed = 0.0
|
||||
elif not _sprint_dust_landing_ready:
|
||||
# Establish a real ground contact before arming landing effects. This
|
||||
# prevents the player's initial placement from looking like a fall.
|
||||
_sprint_dust_landing_ready = grounded
|
||||
elif not grounded:
|
||||
_sprint_dust_airborne = true
|
||||
_sprint_dust_fall_speed = maxf(
|
||||
_sprint_dust_fall_speed,
|
||||
maxf(-velocity.y, 0.0),
|
||||
)
|
||||
elif _sprint_dust_airborne:
|
||||
if _sprint_dust_fall_speed >= LANDING_DUST_MIN_FALL_SPEED:
|
||||
var landing_facing: Vector3 = horizontal_velocity.normalized()
|
||||
if landing_facing.is_zero_approx() and _visuals != null:
|
||||
landing_facing = -_visuals.global_basis.z
|
||||
_sprint_dust.emit_landing_burst(
|
||||
global_position,
|
||||
landing_facing,
|
||||
)
|
||||
_sprint_dust_airborne = false
|
||||
_sprint_dust_fall_speed = 0.0
|
||||
var horizontal_speed_squared: float = horizontal_velocity.length_squared()
|
||||
var fastest_non_sprint_speed := maxf(
|
||||
walk_speed,
|
||||
maxf(sneak_speed, slow_walk_speed)
|
||||
)
|
||||
var running_threshold := fastest_non_sprint_speed + 0.5
|
||||
_sprint_dust.emitting = (
|
||||
is_on_floor()
|
||||
var should_emit: bool = (
|
||||
grounded
|
||||
and not _sitting
|
||||
and not _water_recovery_active
|
||||
and horizontal_speed_squared
|
||||
> running_threshold * running_threshold
|
||||
)
|
||||
_sprint_dust.update_trail(
|
||||
delta,
|
||||
global_position,
|
||||
horizontal_velocity,
|
||||
should_emit,
|
||||
)
|
||||
|
||||
|
||||
func _get_controller_camera_stick() -> Vector2:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
[gd_scene load_steps=21 format=3]
|
||||
[gd_scene load_steps=22 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://player/player.gd" id="1_script"]
|
||||
[ext_resource type="Script" path="res://inventory/fish_inventory.gd" id="2_inventory"]
|
||||
|
|
@ -15,6 +15,7 @@
|
|||
[ext_resource type="PackedScene" path="res://art/exported/characters/base/netfishing_base_character.glb" id="13_character"]
|
||||
[ext_resource type="Script" path="res://progression/player_art_unlocks.gd" id="14_art_unlocks"]
|
||||
[ext_resource type="Script" path="res://progression/player_experience.gd" id="15_experience"]
|
||||
[ext_resource type="Script" path="res://player/sprint_dust_trail.gd" id="16_sprint_dust"]
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="PlayerShape"]
|
||||
radius = 0.45
|
||||
|
|
@ -25,7 +26,7 @@ material = ExtResource("12_blob_shadow")
|
|||
size = Vector2(1.15, 0.72)
|
||||
|
||||
[sub_resource type="Shader" id="SprintDustShader"]
|
||||
code = "shader_type spatial;\nrender_mode blend_mix, depth_draw_never, cull_disabled, unshaded, shadows_disabled;\n\nuniform vec4 dust_color : source_color = vec4(0.47, 0.39, 0.27, 0.42);\nvarying float particle_phase;\n\nvoid vertex() {\n\tparticle_phase = INSTANCE_CUSTOM.y;\n\tMODELVIEW_MATRIX = VIEW_MATRIX * mat4(\n\t\tINV_VIEW_MATRIX[0],\n\t\tINV_VIEW_MATRIX[1],\n\t\tINV_VIEW_MATRIX[2],\n\t\tMODEL_MATRIX[3]\n\t);\n}\n\nvoid fragment() {\n\tvec2 centered_uv = UV * 2.0 - 1.0;\n\tfloat angle = atan(centered_uv.y, centered_uv.x);\n\tfloat edge_wobble = sin(angle * 5.0 + particle_phase * 8.0) * 0.07;\n\tfloat radius = length(centered_uv);\n\tfloat soft_shape = 1.0 - smoothstep(0.48 + edge_wobble, 1.0 + edge_wobble, radius);\n\tfloat fade_in = smoothstep(0.0, 0.12, particle_phase);\n\tfloat fade_out = 1.0 - smoothstep(0.38, 1.0, particle_phase);\n\tALBEDO = dust_color.rgb;\n\tALPHA = soft_shape * dust_color.a * fade_in * fade_out;\n}\n"
|
||||
code = "shader_type spatial;\nrender_mode blend_mix, depth_draw_never, cull_disabled, unshaded, shadows_disabled;\n\nuniform vec3 dust_color : source_color = vec3(0.61, 0.57, 0.53);\nvarying flat float puff_phase;\nvarying flat float puff_alpha;\n\nvoid vertex() {\n\tpuff_phase = INSTANCE_CUSTOM.x;\n\tpuff_alpha = INSTANCE_CUSTOM.w;\n\tVERTEX.xy *= INSTANCE_CUSTOM.yz;\n\tfloat puff_rotation = puff_phase * 6.28318530718;\n\tfloat rotation_sine = sin(puff_rotation);\n\tfloat rotation_cosine = cos(puff_rotation);\n\tVERTEX.xy = mat2(\n\t\tvec2(rotation_cosine, -rotation_sine),\n\t\tvec2(rotation_sine, rotation_cosine)\n\t) * VERTEX.xy;\n\tMODELVIEW_MATRIX = VIEW_MATRIX * mat4(\n\t\tINV_VIEW_MATRIX[0],\n\t\tINV_VIEW_MATRIX[1],\n\t\tINV_VIEW_MATRIX[2],\n\t\tMODEL_MATRIX[3]\n\t);\n}\n\nvoid fragment() {\n\tvec2 centered_uv = UV * 2.0 - 1.0;\n\tfloat angle = atan(centered_uv.y, centered_uv.x);\n\tfloat edge_wobble =\n\t\tsin(angle * 5.0 + puff_phase * 8.0) * 0.07\n\t\t+ sin(angle * 3.0 - puff_phase * 5.0) * 0.025;\n\tfloat radius = length(centered_uv);\n\tif (radius > 0.82 + edge_wobble) {\n\t\tdiscard;\n\t}\n\tALBEDO = dust_color;\n\tALPHA = puff_alpha;\n}\n"
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="SprintDustMaterial"]
|
||||
render_priority = 1
|
||||
|
|
@ -33,7 +34,7 @@ shader = SubResource("SprintDustShader")
|
|||
|
||||
[sub_resource type="QuadMesh" id="SprintDustMesh"]
|
||||
material = SubResource("SprintDustMaterial")
|
||||
size = Vector2(0.38, 0.28)
|
||||
size = Vector2(1, 1)
|
||||
|
||||
[node name="Player" type="CharacterBody3D"]
|
||||
collision_layer = 2
|
||||
|
|
@ -66,27 +67,11 @@ rotation_degrees = Vector3(-90, 0, 0)
|
|||
cast_shadow = 0
|
||||
mesh = SubResource("GroundShadowMesh")
|
||||
|
||||
[node name="SprintDust" type="CPUParticles3D" parent="."]
|
||||
[node name="SprintDust" type="MultiMeshInstance3D" parent="."]
|
||||
unique_name_in_owner = true
|
||||
position = Vector3(0, 0.08, 0)
|
||||
emitting = false
|
||||
amount = 16
|
||||
lifetime = 0.65
|
||||
randomness = 0.55
|
||||
visibility_aabb = AABB(-4, -2, -4, 8, 5, 8)
|
||||
local_coords = false
|
||||
emission_shape = 3
|
||||
emission_box_extents = Vector3(0.24, 0.03, 0.16)
|
||||
direction = Vector3(0, 1, 0)
|
||||
spread = 65.0
|
||||
gravity = Vector3(0, -0.8, 0)
|
||||
initial_velocity_min = 0.35
|
||||
initial_velocity_max = 0.8
|
||||
angular_velocity_min = -90.0
|
||||
angular_velocity_max = 90.0
|
||||
scale_amount_min = 0.35
|
||||
scale_amount_max = 0.72
|
||||
mesh = SubResource("SprintDustMesh")
|
||||
cast_shadow = 0
|
||||
script = ExtResource("16_sprint_dust")
|
||||
puff_mesh = SubResource("SprintDustMesh")
|
||||
|
||||
[node name="CharacterRig" parent="Visuals" instance=ExtResource("13_character")]
|
||||
rotation_degrees = Vector3(0, 180, 0)
|
||||
|
|
|
|||
249
player/sprint_dust_trail.gd
Normal file
249
player/sprint_dust_trail.gd
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
class_name SprintDustTrail
|
||||
extends MultiMeshInstance3D
|
||||
|
||||
const PUFF_COUNT: int = 12
|
||||
const PUFF_SPACING: float = 0.46
|
||||
const PUFF_LIFETIME: float = 0.62
|
||||
const PUFF_GROW_TIME: float = 0.16
|
||||
const PUFF_FADE_TIME: float = 0.22
|
||||
const PUFF_BACK_OFFSET: float = 0.14
|
||||
const PUFF_SIDE_OFFSET: float = 0.11
|
||||
const PUFF_HEIGHT: float = 0.08
|
||||
const PUFF_MIN_SCALE: float = 0.22
|
||||
|
||||
@export var puff_mesh: Mesh
|
||||
|
||||
var _puff_positions: Array[Vector3] = []
|
||||
var _puff_ages := PackedFloat32Array()
|
||||
var _puff_widths := PackedFloat32Array()
|
||||
var _puff_heights := PackedFloat32Array()
|
||||
var _puff_phases := PackedFloat32Array()
|
||||
var _next_puff: int = 0
|
||||
var _spawn_serial: int = 0
|
||||
var _foot_side: float = -1.0
|
||||
var _tracking_run: bool = false
|
||||
var _previous_source_position: Vector3
|
||||
var _distance_since_puff: float = 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
top_level = true
|
||||
global_transform = Transform3D.IDENTITY
|
||||
_initialize_pool()
|
||||
|
||||
|
||||
func update_trail(
|
||||
delta: float,
|
||||
source_position: Vector3,
|
||||
movement_velocity: Vector3,
|
||||
is_running: bool,
|
||||
) -> void:
|
||||
_update_existing_puffs(delta)
|
||||
custom_aabb = AABB(
|
||||
source_position - Vector3(5.0, 2.0, 5.0),
|
||||
Vector3(10.0, 5.0, 10.0),
|
||||
)
|
||||
if not is_running:
|
||||
_tracking_run = false
|
||||
_distance_since_puff = 0.0
|
||||
return
|
||||
|
||||
var flat_source := Vector3(
|
||||
source_position.x,
|
||||
source_position.y,
|
||||
source_position.z,
|
||||
)
|
||||
if not _tracking_run:
|
||||
_tracking_run = true
|
||||
_previous_source_position = flat_source
|
||||
_distance_since_puff = 0.0
|
||||
return
|
||||
|
||||
var segment := flat_source - _previous_source_position
|
||||
segment.y = 0.0
|
||||
var remaining_distance: float = segment.length()
|
||||
if remaining_distance <= 0.0001:
|
||||
_previous_source_position = flat_source
|
||||
return
|
||||
|
||||
var segment_direction: Vector3 = segment / remaining_distance
|
||||
var movement_direction := Vector3(
|
||||
movement_velocity.x,
|
||||
0.0,
|
||||
movement_velocity.z,
|
||||
).normalized()
|
||||
if movement_direction.is_zero_approx():
|
||||
movement_direction = segment_direction
|
||||
var cursor: Vector3 = _previous_source_position
|
||||
var distance_to_next: float = PUFF_SPACING - _distance_since_puff
|
||||
while remaining_distance >= distance_to_next:
|
||||
cursor += segment_direction * distance_to_next
|
||||
_spawn_puff(cursor, movement_direction)
|
||||
remaining_distance -= distance_to_next
|
||||
_distance_since_puff = 0.0
|
||||
distance_to_next = PUFF_SPACING
|
||||
_distance_since_puff += remaining_distance
|
||||
_previous_source_position = flat_source
|
||||
|
||||
|
||||
func clear_trail() -> void:
|
||||
_tracking_run = false
|
||||
_distance_since_puff = 0.0
|
||||
for index: int in PUFF_COUNT:
|
||||
_puff_ages[index] = PUFF_LIFETIME
|
||||
_write_inactive_puff(index)
|
||||
|
||||
|
||||
func emit_landing_burst(
|
||||
source_position: Vector3,
|
||||
facing_direction: Vector3,
|
||||
) -> void:
|
||||
var direction := Vector3(
|
||||
facing_direction.x,
|
||||
0.0,
|
||||
facing_direction.z,
|
||||
).normalized()
|
||||
if direction.is_zero_approx():
|
||||
direction = Vector3.FORWARD
|
||||
var first_direction: Vector3 = -direction
|
||||
for puff_index: int in 5:
|
||||
var spread_direction := first_direction.rotated(
|
||||
Vector3.UP,
|
||||
TAU * float(puff_index) / 5.0,
|
||||
)
|
||||
var radius: float = lerpf(
|
||||
0.38,
|
||||
0.46,
|
||||
_stable_variation(_spawn_serial + puff_index + 81),
|
||||
)
|
||||
_spawn_puff_at(
|
||||
source_position
|
||||
+ spread_direction * radius
|
||||
+ Vector3.UP * PUFF_HEIGHT,
|
||||
0.95,
|
||||
)
|
||||
|
||||
|
||||
func get_active_puff_count() -> int:
|
||||
var active_count: int = 0
|
||||
for age: float in _puff_ages:
|
||||
if age < PUFF_LIFETIME:
|
||||
active_count += 1
|
||||
return active_count
|
||||
|
||||
|
||||
func get_first_active_puff_position() -> Vector3:
|
||||
for index: int in PUFF_COUNT:
|
||||
if _puff_ages[index] < PUFF_LIFETIME:
|
||||
return _puff_positions[index]
|
||||
return Vector3.INF
|
||||
|
||||
|
||||
func _initialize_pool() -> void:
|
||||
var pool := MultiMesh.new()
|
||||
pool.transform_format = MultiMesh.TRANSFORM_3D
|
||||
pool.use_custom_data = true
|
||||
pool.instance_count = PUFF_COUNT
|
||||
pool.mesh = puff_mesh
|
||||
multimesh = pool
|
||||
_puff_positions.resize(PUFF_COUNT)
|
||||
_puff_ages.resize(PUFF_COUNT)
|
||||
_puff_widths.resize(PUFF_COUNT)
|
||||
_puff_heights.resize(PUFF_COUNT)
|
||||
_puff_phases.resize(PUFF_COUNT)
|
||||
clear_trail()
|
||||
|
||||
|
||||
func _spawn_puff(source_position: Vector3, direction: Vector3) -> void:
|
||||
var lateral := Vector3(-direction.z, 0.0, direction.x)
|
||||
var position := (
|
||||
source_position
|
||||
- direction * PUFF_BACK_OFFSET
|
||||
+ lateral * PUFF_SIDE_OFFSET * _foot_side
|
||||
+ Vector3.UP * PUFF_HEIGHT
|
||||
)
|
||||
_spawn_puff_at(position)
|
||||
_foot_side *= -1.0
|
||||
|
||||
|
||||
func _spawn_puff_at(
|
||||
position: Vector3,
|
||||
size_multiplier: float = 1.0,
|
||||
) -> void:
|
||||
var variation: float = _stable_variation(_spawn_serial)
|
||||
var second_variation: float = _stable_variation(_spawn_serial + 37)
|
||||
var index: int = _next_puff
|
||||
_puff_positions[index] = position
|
||||
_puff_ages[index] = 0.0
|
||||
_puff_widths[index] = (
|
||||
lerpf(0.34, 0.43, variation) * size_multiplier
|
||||
)
|
||||
_puff_heights[index] = (
|
||||
lerpf(0.23, 0.30, second_variation) * size_multiplier
|
||||
)
|
||||
_puff_phases[index] = variation
|
||||
_write_puff(index, PUFF_MIN_SCALE, 1.0)
|
||||
_next_puff = (_next_puff + 1) % PUFF_COUNT
|
||||
_spawn_serial += 1
|
||||
|
||||
|
||||
func _update_existing_puffs(delta: float) -> void:
|
||||
if multimesh == null:
|
||||
return
|
||||
for index: int in PUFF_COUNT:
|
||||
var age: float = _puff_ages[index]
|
||||
if age >= PUFF_LIFETIME:
|
||||
continue
|
||||
age += delta
|
||||
_puff_ages[index] = age
|
||||
if age >= PUFF_LIFETIME:
|
||||
_write_inactive_puff(index)
|
||||
continue
|
||||
var visual_scale: float = 1.0
|
||||
if age < PUFF_GROW_TIME:
|
||||
var grow_weight: float = smoothstep(
|
||||
0.0,
|
||||
1.0,
|
||||
age / PUFF_GROW_TIME,
|
||||
)
|
||||
visual_scale = lerpf(PUFF_MIN_SCALE, 1.0, grow_weight)
|
||||
var visual_alpha: float = 1.0
|
||||
if age > PUFF_LIFETIME - PUFF_FADE_TIME:
|
||||
var fade_weight: float = smoothstep(
|
||||
0.0,
|
||||
1.0,
|
||||
(age - (PUFF_LIFETIME - PUFF_FADE_TIME))
|
||||
/ PUFF_FADE_TIME,
|
||||
)
|
||||
visual_alpha = 1.0 - fade_weight
|
||||
_write_puff(index, visual_scale, visual_alpha)
|
||||
|
||||
|
||||
func _write_puff(
|
||||
index: int,
|
||||
visual_scale: float,
|
||||
visual_alpha: float,
|
||||
) -> void:
|
||||
var puff_transform := Transform3D.IDENTITY
|
||||
puff_transform.origin = _puff_positions[index]
|
||||
multimesh.set_instance_transform(index, puff_transform)
|
||||
multimesh.set_instance_custom_data(
|
||||
index,
|
||||
Color(
|
||||
_puff_phases[index],
|
||||
_puff_widths[index] * visual_scale,
|
||||
_puff_heights[index] * visual_scale,
|
||||
visual_alpha,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
func _write_inactive_puff(index: int) -> void:
|
||||
var inactive_transform := Transform3D.IDENTITY
|
||||
inactive_transform.origin = _puff_positions[index]
|
||||
multimesh.set_instance_transform(index, inactive_transform)
|
||||
multimesh.set_instance_custom_data(index, Color(0.0, 0.0, 0.0, 0.0))
|
||||
|
||||
|
||||
func _stable_variation(serial: int) -> float:
|
||||
return fposmod(sin(float(serial + 1) * 12.9898) * 43758.5453, 1.0)
|
||||
1
player/sprint_dust_trail.gd.uid
Normal file
1
player/sprint_dust_trail.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://o2gi5oqunsp8
|
||||
|
|
@ -27,6 +27,55 @@ func _run() -> void:
|
|||
await physics_frame
|
||||
|
||||
var player := main.get("_player") as Player
|
||||
var sprint_dust := player.get_node("%SprintDust") as SprintDustTrail
|
||||
assert(sprint_dust != null)
|
||||
assert(sprint_dust.get_active_puff_count() == 0)
|
||||
sprint_dust.update_trail(
|
||||
0.016,
|
||||
Vector3.ZERO,
|
||||
Vector3(4.5, 0.0, 0.0),
|
||||
true,
|
||||
)
|
||||
assert(sprint_dust.get_active_puff_count() == 0)
|
||||
sprint_dust.update_trail(
|
||||
0.016,
|
||||
Vector3(0.5, 0.0, 0.0),
|
||||
Vector3(4.5, 0.0, 0.0),
|
||||
true,
|
||||
)
|
||||
assert(sprint_dust.get_active_puff_count() == 1)
|
||||
var planted_puff_position: Vector3 = (
|
||||
sprint_dust.get_first_active_puff_position()
|
||||
)
|
||||
sprint_dust.update_trail(
|
||||
0.08,
|
||||
Vector3(0.75, 0.0, 0.0),
|
||||
Vector3(4.5, 0.0, 0.0),
|
||||
true,
|
||||
)
|
||||
assert(
|
||||
sprint_dust.get_first_active_puff_position()
|
||||
== planted_puff_position
|
||||
)
|
||||
sprint_dust.update_trail(
|
||||
0.08,
|
||||
Vector3(0.75, 0.0, 0.0),
|
||||
Vector3.ZERO,
|
||||
false,
|
||||
)
|
||||
assert(sprint_dust.get_active_puff_count() == 1)
|
||||
sprint_dust.clear_trail()
|
||||
assert(sprint_dust.get_active_puff_count() == 0)
|
||||
sprint_dust.emit_landing_burst(Vector3.ZERO, Vector3.FORWARD)
|
||||
assert(sprint_dust.get_active_puff_count() == 5)
|
||||
sprint_dust.update_trail(
|
||||
0.08,
|
||||
Vector3.ZERO,
|
||||
Vector3.ZERO,
|
||||
false,
|
||||
)
|
||||
assert(sprint_dust.get_active_puff_count() == 5)
|
||||
sprint_dust.clear_trail()
|
||||
var service := main.get_node(
|
||||
"%NetworkSurfaceDrawingService"
|
||||
) as NetworkSurfaceDrawingService
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue