Add Phase 19 Starter Scene Composition UI/UX Specification and Tests
- Introduced a new UI/UX specification document for the Phase 19 Starter Scene Composition, detailing user paths, composition rules for Town Street and Living Room, camera framing, and observable acceptance outcomes. - Implemented a visual capture test for different sky states in the sandbox stage, ensuring proper rendering and visual consistency. - Created a comprehensive test suite for the Phase 19 starter scene composition, validating scene loading, geometry, navigation, and interaction behaviors for both Town Street and Living Room. - Added checks for visual alignment, object placements, and scene transitions to ensure adherence to the new UI/UX specifications.
This commit is contained in:
@@ -2660,8 +2660,6 @@ func _apply_scene_data(data: Dictionary, source_path: String, is_recovery: bool)
|
||||
_reset_scene_content()
|
||||
var adapter_result: Dictionary = _scene_adapter.apply(data, _world, _spawner)
|
||||
_sky_id = StringName(String(adapter_result.get("sky_id", "sky_neutral")))
|
||||
if _sky_view != null:
|
||||
_sky_view.set_sky(_sky_id)
|
||||
var loaded_nodes: Array[Node2D] = adapter_result.get("loaded_nodes", [])
|
||||
for node: Node2D in loaded_nodes:
|
||||
_finalize_placed_node(node)
|
||||
@@ -2685,6 +2683,8 @@ func _apply_scene_data(data: Dictionary, source_path: String, is_recovery: bool)
|
||||
var camera_data: Dictionary = raw_camera as Dictionary if raw_camera is Dictionary else {}
|
||||
_camera.global_position = STAGE_SCENE_IO.data_to_vector(camera_data.get("position"), DEFAULT_CAMERA_POSITION)
|
||||
_camera.zoom = Vector2.ONE * clampf(float(camera_data.get("zoom", DEFAULT_CAMERA_ZOOM)), min_zoom, max_zoom)
|
||||
if _sky_view != null:
|
||||
_sky_view.set_sky(_sky_id)
|
||||
var raw_metadata: Variant = data.get("metadata", {})
|
||||
var metadata: Dictionary = raw_metadata as Dictionary if raw_metadata is Dictionary else {}
|
||||
if _document != null:
|
||||
@@ -4550,6 +4550,7 @@ func _build_sky_layer() -> void:
|
||||
_sky_view.set_sky(_sky_id)
|
||||
_sky_layer.add_child(_sky_view)
|
||||
add_child(_sky_layer)
|
||||
_sky_view.bind_camera(_camera)
|
||||
|
||||
|
||||
func _build_placement_overlay() -> void:
|
||||
@@ -4591,29 +4592,47 @@ func _rebake_navigation() -> void:
|
||||
if block == null:
|
||||
continue
|
||||
var pts := PackedVector2Array()
|
||||
var block_top_y: float = INF
|
||||
for p: Vector2 in block.polygon_points:
|
||||
pts.append(block.transform * p)
|
||||
var world_point: Vector2 = block.transform * p
|
||||
pts.append(world_point)
|
||||
block_top_y = minf(block_top_y, world_point.y)
|
||||
for i: int in pts.size():
|
||||
var a := pts[i]
|
||||
var b := pts[(i + 1) % pts.size()]
|
||||
var edge := b - a
|
||||
if edge.length_squared() <= 0.001:
|
||||
continue
|
||||
if absf(a.y - b.y) <= 0.001:
|
||||
if is_equal_approx(a.y, block_top_y):
|
||||
surfaces.append({"a": a, "b": b, "normal": Vector2.UP})
|
||||
continue
|
||||
# TerrainUtils stores clockwise polygons; this right normal is
|
||||
# outward, so only its upward-facing edges are walkable.
|
||||
# outward on sloped edges, so only upward-facing slopes are walkable.
|
||||
var outward := Vector2(edge.y, -edge.x).normalized()
|
||||
if outward.y > -0.15:
|
||||
continue
|
||||
surfaces.append({"a": a, "b": b, "normal": outward})
|
||||
for surface: Dictionary in surfaces:
|
||||
surfaces = _merge_contiguous_horizontal_navigation_surfaces(surfaces)
|
||||
var endpoint_steps: Array[Dictionary] = _find_navigation_endpoint_steps(surfaces)
|
||||
var ribbon_surfaces: Array[Dictionary] = _split_navigation_surfaces_at_step_edges(surfaces, endpoint_steps)
|
||||
for surface: Dictionary in ribbon_surfaces:
|
||||
_append_navigation_ribbon(verts, triangles, surface["a"], surface["b"], surface["normal"])
|
||||
for endpoint_step: Dictionary in endpoint_steps:
|
||||
_append_navigation_endpoint_step_ribbon(
|
||||
verts,
|
||||
triangles,
|
||||
endpoint_step["upper_point"],
|
||||
endpoint_step["lower_y"],
|
||||
endpoint_step["outward_normal"]
|
||||
)
|
||||
# Join only short, climbable gaps between exposed surface ends. This connects
|
||||
# authored stair treads without making cliffs or terrain interiors walkable.
|
||||
for i: int in surfaces.size():
|
||||
for j: int in range(i + 1, surfaces.size()):
|
||||
for a: Vector2 in [surfaces[i]["a"], surfaces[i]["b"]]:
|
||||
for b: Vector2 in [surfaces[j]["a"], surfaces[j]["b"]]:
|
||||
if absf(a.x - b.x) <= 64.0 and absf(a.y - b.y) <= 96.0:
|
||||
if absf(a.x - b.x) > 0.001 and absf(a.x - b.x) <= 64.0 and absf(a.y - b.y) <= 96.0:
|
||||
_append_navigation_ribbon(verts, triangles, a, b, Vector2.UP)
|
||||
var poly := NavigationPolygon.new()
|
||||
poly.set_vertices(verts)
|
||||
@@ -4624,6 +4643,161 @@ func _rebake_navigation() -> void:
|
||||
print("[nav] baked verts=%d polys=%d" % [verts.size(), triangles.size()])
|
||||
|
||||
|
||||
func _find_navigation_endpoint_steps(surfaces: Array[Dictionary]) -> Array[Dictionary]:
|
||||
var endpoint_steps: Array[Dictionary] = []
|
||||
var horizontal: Array[Dictionary] = []
|
||||
for surface: Dictionary in surfaces:
|
||||
var a: Vector2 = surface.get("a", Vector2.ZERO)
|
||||
var b: Vector2 = surface.get("b", Vector2.ZERO)
|
||||
var normal: Vector2 = surface.get("normal", Vector2.ZERO)
|
||||
if absf(a.y - b.y) <= 0.001 and normal.is_equal_approx(Vector2.UP):
|
||||
horizontal.append(surface)
|
||||
for upper: Dictionary in horizontal:
|
||||
var upper_a: Vector2 = upper["a"]
|
||||
var upper_b: Vector2 = upper["b"]
|
||||
for lower: Dictionary in horizontal:
|
||||
var lower_a: Vector2 = lower["a"]
|
||||
var lower_b: Vector2 = lower["b"]
|
||||
var vertical_step: float = lower_a.y - upper_a.y
|
||||
if vertical_step <= 0.001 or vertical_step > 96.0:
|
||||
continue
|
||||
# A lower walk surface can join an upper row only where it extends
|
||||
# past that row's end. The vertical nav strip then represents the
|
||||
# physically exposed step/drop beside the upper terrain collider.
|
||||
if lower_a.x < upper_a.x - 0.01 and lower_b.x >= upper_a.x - 0.01:
|
||||
endpoint_steps.append({
|
||||
"upper_point": upper_a,
|
||||
"lower_y": lower_a.y,
|
||||
"outward_normal": Vector2.LEFT,
|
||||
})
|
||||
if lower_b.x > upper_b.x + 0.01 and lower_a.x <= upper_b.x + 0.01:
|
||||
endpoint_steps.append({
|
||||
"upper_point": upper_b,
|
||||
"lower_y": lower_a.y,
|
||||
"outward_normal": Vector2.RIGHT,
|
||||
})
|
||||
return endpoint_steps
|
||||
|
||||
|
||||
func _split_navigation_surfaces_at_step_edges(
|
||||
surfaces: Array[Dictionary],
|
||||
endpoint_steps: Array[Dictionary]
|
||||
) -> Array[Dictionary]:
|
||||
var result: Array[Dictionary] = []
|
||||
for surface: Dictionary in surfaces:
|
||||
var a: Vector2 = surface.get("a", Vector2.ZERO)
|
||||
var b: Vector2 = surface.get("b", Vector2.ZERO)
|
||||
var normal: Vector2 = surface.get("normal", Vector2.ZERO)
|
||||
if absf(a.y - b.y) > 0.001 or not normal.is_equal_approx(Vector2.UP):
|
||||
result.append(surface)
|
||||
continue
|
||||
var split_x: Array[float] = [a.x, b.x]
|
||||
for endpoint_step: Dictionary in endpoint_steps:
|
||||
if not is_equal_approx(float(endpoint_step["lower_y"]), a.y):
|
||||
continue
|
||||
var upper_point: Vector2 = endpoint_step["upper_point"]
|
||||
var outward_normal: Vector2 = endpoint_step["outward_normal"]
|
||||
var outer_x: float = upper_point.x + outward_normal.x * 16.0
|
||||
if upper_point.x > a.x and upper_point.x < b.x:
|
||||
split_x.append(upper_point.x)
|
||||
if outer_x > a.x and outer_x < b.x:
|
||||
split_x.append(outer_x)
|
||||
split_x.sort()
|
||||
var unique_split_x: Array[float] = []
|
||||
for split: float in split_x:
|
||||
if unique_split_x.is_empty() or not is_equal_approx(split, unique_split_x[unique_split_x.size() - 1]):
|
||||
unique_split_x.append(split)
|
||||
for split_index: int in range(unique_split_x.size() - 1):
|
||||
var segment_start: float = unique_split_x[split_index]
|
||||
var segment_end: float = unique_split_x[split_index + 1]
|
||||
if segment_end - segment_start <= 0.001:
|
||||
continue
|
||||
result.append({
|
||||
"a": Vector2(segment_start, a.y),
|
||||
"b": Vector2(segment_end, a.y),
|
||||
"normal": Vector2.UP,
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
func _append_navigation_endpoint_step_ribbon(
|
||||
verts: PackedVector2Array,
|
||||
triangles: Array[PackedInt32Array],
|
||||
upper_point: Vector2,
|
||||
lower_y: float,
|
||||
outward_normal: Vector2
|
||||
) -> void:
|
||||
var inner_x: float = upper_point.x
|
||||
var outer_x: float = inner_x + outward_normal.x * 16.0
|
||||
var upper_nav_top_y: float = upper_point.y - 4.0
|
||||
var upper_nav_bottom_y: float = upper_point.y + 12.0
|
||||
var lower_nav_top_y: float = lower_y - 4.0
|
||||
var base: int = verts.size()
|
||||
if outward_normal.x < 0.0:
|
||||
verts.append_array(PackedVector2Array([
|
||||
Vector2(outer_x, upper_nav_top_y),
|
||||
Vector2(inner_x, upper_nav_top_y),
|
||||
Vector2(inner_x, upper_nav_bottom_y),
|
||||
Vector2(inner_x, lower_nav_top_y),
|
||||
Vector2(outer_x, lower_nav_top_y),
|
||||
]))
|
||||
triangles.append(PackedInt32Array([base, base + 1, base + 2]))
|
||||
triangles.append(PackedInt32Array([base, base + 2, base + 3]))
|
||||
triangles.append(PackedInt32Array([base, base + 3, base + 4]))
|
||||
else:
|
||||
verts.append_array(PackedVector2Array([
|
||||
Vector2(inner_x, upper_nav_top_y),
|
||||
Vector2(outer_x, upper_nav_top_y),
|
||||
Vector2(outer_x, lower_nav_top_y),
|
||||
Vector2(inner_x, lower_nav_top_y),
|
||||
Vector2(inner_x, upper_nav_bottom_y),
|
||||
]))
|
||||
triangles.append(PackedInt32Array([base, base + 1, base + 2]))
|
||||
triangles.append(PackedInt32Array([base, base + 2, base + 4]))
|
||||
triangles.append(PackedInt32Array([base + 2, base + 3, base + 4]))
|
||||
|
||||
|
||||
func _merge_contiguous_horizontal_navigation_surfaces(surfaces: Array[Dictionary]) -> Array[Dictionary]:
|
||||
var horizontal: Array[Dictionary] = []
|
||||
var other: Array[Dictionary] = []
|
||||
for surface: Dictionary in surfaces:
|
||||
var a: Vector2 = surface.get("a", Vector2.ZERO)
|
||||
var b: Vector2 = surface.get("b", Vector2.ZERO)
|
||||
var normal: Vector2 = surface.get("normal", Vector2.ZERO)
|
||||
if absf(a.y - b.y) > 0.001 or not normal.is_equal_approx(Vector2.UP):
|
||||
other.append(surface)
|
||||
continue
|
||||
horizontal.append({
|
||||
"a": Vector2(minf(a.x, b.x), a.y),
|
||||
"b": Vector2(maxf(a.x, b.x), a.y),
|
||||
"normal": Vector2.UP,
|
||||
})
|
||||
horizontal.sort_custom(_navigation_surface_precedes)
|
||||
var merged: Array[Dictionary] = []
|
||||
for surface: Dictionary in horizontal:
|
||||
if not merged.is_empty():
|
||||
var previous_index: int = merged.size() - 1
|
||||
var previous: Dictionary = merged[previous_index]
|
||||
var previous_a: Vector2 = previous["a"]
|
||||
var previous_b: Vector2 = previous["b"]
|
||||
var current_a: Vector2 = surface["a"]
|
||||
if is_equal_approx(previous_a.y, current_a.y) and absf(previous_b.x - current_a.x) <= 0.01:
|
||||
previous["b"] = surface["b"]
|
||||
merged[previous_index] = previous
|
||||
continue
|
||||
merged.append(surface)
|
||||
merged.append_array(other)
|
||||
return merged
|
||||
|
||||
|
||||
func _navigation_surface_precedes(left: Dictionary, right: Dictionary) -> bool:
|
||||
var left_a: Vector2 = left.get("a", Vector2.ZERO)
|
||||
var right_a: Vector2 = right.get("a", Vector2.ZERO)
|
||||
if not is_equal_approx(left_a.y, right_a.y):
|
||||
return left_a.y < right_a.y
|
||||
return left_a.x < right_a.x
|
||||
|
||||
|
||||
func _append_navigation_ribbon(
|
||||
verts: PackedVector2Array,
|
||||
triangles: Array[PackedInt32Array],
|
||||
|
||||
@@ -5,27 +5,54 @@ extends Control
|
||||
const SKY_IDS: Array[StringName] = [&"sky_clear_day", &"sky_cloudy_day", &"sky_sunset", &"sky_starry_night", &"sky_neutral"]
|
||||
const GRADIENT_TEXTURE_HEIGHT: int = 2048
|
||||
const CLOUD_VIEWBOX: Vector2 = Vector2(210.0, 110.0)
|
||||
const BASE_VIEWPORT_SIZE: Vector2 = Vector2(800.0, 480.0)
|
||||
const REF_POSITION: Vector2 = Vector2(0.0, -400.0)
|
||||
const REF_ZOOM: float = 0.5
|
||||
const NIGHT_STAR_COLOR: Color = Color("#FFF4CE")
|
||||
const CAMERA_RESPONSE_PROFILES: Dictionary = {
|
||||
&"sky_clear_day": {"pan_limits": Vector2(0.025, 0.015), "pan_spans": Vector2(0.8, 0.8), "zoom_gain": 0.10, "zoom_span": 1.2},
|
||||
&"sky_cloudy_day": {"pan_limits": Vector2(0.020, 0.012), "pan_spans": Vector2(0.85, 0.85), "zoom_gain": 0.075, "zoom_span": 1.2},
|
||||
&"sky_sunset": {"pan_limits": Vector2(0.030, 0.018), "pan_spans": Vector2(0.9, 0.9), "zoom_gain": 0.11, "zoom_span": 1.2},
|
||||
&"sky_starry_night": {"pan_limits": Vector2(0.035, 0.025), "pan_spans": Vector2(1.0, 1.0), "zoom_gain": 0.12, "zoom_span": 1.2},
|
||||
&"sky_neutral": {"pan_limits": Vector2.ZERO, "pan_spans": Vector2.ONE, "zoom_gain": 0.0, "zoom_span": 1.0},
|
||||
}
|
||||
|
||||
var _sky_id: StringName = &"sky_neutral"
|
||||
var _stage_theme: SandboxTheme
|
||||
var _gradient_texture: GradientTexture2D
|
||||
var _bound_camera: Camera2D
|
||||
var _motif_shift: Vector2 = Vector2.ZERO
|
||||
var _motif_scale: float = 1.0
|
||||
var _cached_cloud_outline: PackedVector2Array = PackedVector2Array()
|
||||
var _cached_cloud_underside: PackedVector2Array = PackedVector2Array()
|
||||
var _cached_cloud_accents: Array[PackedVector2Array] = []
|
||||
var _cached_moon_outline: PackedVector2Array = PackedVector2Array()
|
||||
var _cached_moon_outer_arc: PackedVector2Array = PackedVector2Array()
|
||||
var _cached_moon_warm_rim: PackedVector2Array = PackedVector2Array()
|
||||
var _cached_moon_highlight: PackedVector2Array = PackedVector2Array()
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
|
||||
_cache_motif_geometry()
|
||||
_update_gradient_texture()
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func bind_camera(camera: Camera2D) -> void:
|
||||
_bound_camera = camera
|
||||
set_process(camera != null)
|
||||
_refresh_camera_response(true)
|
||||
|
||||
|
||||
func set_sky(id: StringName) -> void:
|
||||
var next_sky_id: StringName = id if SKY_IDS.has(id) else &"sky_neutral"
|
||||
if _sky_id == next_sky_id:
|
||||
return
|
||||
var changed: bool = _sky_id != next_sky_id
|
||||
_sky_id = next_sky_id
|
||||
_update_gradient_texture()
|
||||
queue_redraw()
|
||||
if changed:
|
||||
_update_gradient_texture()
|
||||
_refresh_camera_response(true)
|
||||
|
||||
|
||||
func get_sky() -> StringName:
|
||||
@@ -39,9 +66,73 @@ func apply_theme(theme: SandboxTheme) -> void:
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what == NOTIFICATION_RESIZED:
|
||||
_refresh_camera_response(true)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
_refresh_camera_response(false)
|
||||
|
||||
|
||||
static func compute_camera_response(sky_id: StringName, camera_position: Vector2, camera_zoom: float, viewport_size: Vector2) -> Dictionary:
|
||||
var fallback: Dictionary = {"shift": Vector2.ZERO, "scale": 1.0}
|
||||
if not camera_position.is_finite() or not viewport_size.is_finite() or viewport_size.x <= 0.0 or viewport_size.y <= 0.0:
|
||||
return fallback
|
||||
if not is_finite(camera_zoom) or camera_zoom <= 0.0:
|
||||
return fallback
|
||||
var profile_value: Variant = CAMERA_RESPONSE_PROFILES.get(sky_id, CAMERA_RESPONSE_PROFILES[&"sky_neutral"])
|
||||
var profile: Dictionary = profile_value as Dictionary
|
||||
var pan_limits: Vector2 = profile.get("pan_limits", Vector2.ZERO)
|
||||
var pan_spans: Vector2 = profile.get("pan_spans", Vector2.ONE)
|
||||
var zoom_gain: float = float(profile.get("zoom_gain", 0.0))
|
||||
var zoom_span: float = maxf(float(profile.get("zoom_span", 1.0)), 0.001)
|
||||
var reference_frame_delta: Vector2 = (camera_position - REF_POSITION) * REF_ZOOM
|
||||
var normalized_pan: Vector2 = reference_frame_delta / viewport_size
|
||||
var shift: Vector2 = Vector2(
|
||||
-viewport_size.x * pan_limits.x * tanh(normalized_pan.x / maxf(pan_spans.x, 0.001)),
|
||||
-viewport_size.y * pan_limits.y * tanh(normalized_pan.y / maxf(pan_spans.y, 0.001))
|
||||
)
|
||||
var log_zoom: float = log(camera_zoom / REF_ZOOM)
|
||||
var motif_scale: float = exp(zoom_gain * tanh(log_zoom / zoom_span))
|
||||
return {"shift": shift, "scale": motif_scale}
|
||||
|
||||
|
||||
static func star_anchor_for_index(index: int) -> Vector2:
|
||||
return Vector2(
|
||||
fposmod(float(index) * 0.61803398875 + 0.13, 1.0),
|
||||
fposmod(float(index) * 0.41421356237 + 0.08, 0.62)
|
||||
)
|
||||
|
||||
|
||||
func _refresh_camera_response(force_redraw: bool) -> void:
|
||||
var next_shift: Vector2 = Vector2.ZERO
|
||||
var next_scale: float = 1.0
|
||||
if _bound_camera != null and is_instance_valid(_bound_camera):
|
||||
var camera_position: Vector2 = _bound_camera.global_position
|
||||
var camera_zoom: Vector2 = _bound_camera.zoom
|
||||
if camera_position.is_finite() and camera_zoom.is_finite() and camera_zoom.x > 0.0 and camera_zoom.y > 0.0 and is_equal_approx(camera_zoom.x, camera_zoom.y):
|
||||
var response: Dictionary = compute_camera_response(_sky_id, camera_position, camera_zoom.x, size)
|
||||
next_shift = response.get("shift", Vector2.ZERO)
|
||||
next_scale = float(response.get("scale", 1.0))
|
||||
var changed: bool = not _motif_shift.is_equal_approx(next_shift) or not is_equal_approx(_motif_scale, next_scale)
|
||||
_motif_shift = next_shift
|
||||
_motif_scale = next_scale
|
||||
if force_redraw or changed:
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _cache_motif_geometry() -> void:
|
||||
_cached_cloud_outline = _build_cloud_outline()
|
||||
_cached_cloud_underside = _build_cloud_underside()
|
||||
_cached_cloud_accents.clear()
|
||||
for accent_curve: PackedVector2Array in _build_cloud_accents():
|
||||
_cached_cloud_accents.append(_sample_curve_chain([accent_curve], 12))
|
||||
_cached_moon_outline = _sample_curve_chain(_build_moon_curves(), 16)
|
||||
_cached_moon_outer_arc = _sample_curve_chain(_build_moon_outer_arc_curves(), 16)
|
||||
_cached_moon_warm_rim = _build_moon_warm_rim(_cached_moon_outer_arc, 7.5)
|
||||
_cached_moon_highlight = _sample_curve_chain(_build_moon_highlight(), 18)
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if size.x <= 0.0 or size.y <= 0.0:
|
||||
return
|
||||
@@ -90,8 +181,9 @@ func _update_gradient_texture() -> void:
|
||||
|
||||
|
||||
func _draw_sun() -> void:
|
||||
var center: Vector2 = Vector2(size.x * 0.78125, size.y * (0.241 if _sky_id == &"sky_sunset" else 0.242))
|
||||
var radius: float = size.y * (0.0979 if _sky_id == &"sky_sunset" else 0.0792)
|
||||
var center_anchor: Vector2 = Vector2(0.72, 0.241 if _sky_id == &"sky_sunset" else 0.242)
|
||||
var center: Vector2 = _motif_anchor(center_anchor)
|
||||
var radius: float = _base_motif_scale() * _motif_scale * (47.0 if _sky_id == &"sky_sunset" else 38.0)
|
||||
var halo_color: Color = Color("#FFE1A2") if _sky_id == &"sky_sunset" else Color("#FFF1B3")
|
||||
var sun_color: Color = Color("#FFE296") if _sky_id == &"sky_sunset" else Color("#FFE990")
|
||||
draw_circle(center, radius * 1.5, Color(halo_color.r, halo_color.g, halo_color.b, 0.14))
|
||||
@@ -101,87 +193,87 @@ func _draw_sun() -> void:
|
||||
|
||||
func _draw_stars() -> void:
|
||||
for index: int in 48:
|
||||
var x: float = fposmod(float(index * 137 + 39), size.x)
|
||||
var y: float = fposmod(float(index * 83 + 21), size.y * 0.62)
|
||||
draw_circle(Vector2(x, y), 1.3 if index % 4 != 0 else 2.0, NIGHT_STAR_COLOR)
|
||||
var anchor: Vector2 = star_anchor_for_index(index)
|
||||
var center: Vector2 = _motif_anchor(anchor)
|
||||
var radius: float = _base_motif_scale() * _motif_scale * (1.3 if index % 4 != 0 else 2.0)
|
||||
draw_circle(center, radius, NIGHT_STAR_COLOR)
|
||||
for star_index: int in 3:
|
||||
var center: Vector2 = Vector2(size.x * [0.3675, 0.9425, 0.22125][star_index], size.y * [0.085, 0.229, 0.348][star_index])
|
||||
var radius: float = maxf(1.3, size.y / 480.0 * 7.0)
|
||||
var anchors: Array[Vector2] = [Vector2(0.3675, 0.085), Vector2(0.9425, 0.229), Vector2(0.22125, 0.348)]
|
||||
var center: Vector2 = _motif_anchor(anchors[star_index])
|
||||
var radius: float = _base_motif_scale() * _motif_scale * 7.0
|
||||
draw_line(center - Vector2(0.0, radius), center + Vector2(0.0, radius), Color("#FFEABB"), maxf(1.0, radius * 0.22), true)
|
||||
draw_line(center - Vector2(radius, 0.0), center + Vector2(radius, 0.0), Color("#FFEABB"), maxf(1.0, radius * 0.22), true)
|
||||
|
||||
|
||||
func _draw_clouds() -> void:
|
||||
var origins: Array[Vector4] = []
|
||||
var origins: Array[Vector3] = []
|
||||
var opacities: Array[float] = []
|
||||
var outer_color: Color
|
||||
var underside_color: Color
|
||||
var accent_color: Color
|
||||
match _sky_id:
|
||||
&"sky_clear_day":
|
||||
origins = [Vector4(42.0, 66.0, 205.0, 108.0), Vector4(282.0, 167.0, 244.0, 127.0), Vector4(612.0, 288.0, 187.0, 98.0)]
|
||||
origins = [Vector3(0.18, 0.17, 200.0), Vector3(0.51, 0.33, 220.0), Vector3(0.88, 0.48, 174.0)]
|
||||
opacities = [0.93, 0.88, 0.8]
|
||||
outer_color = Color("#FFFFFF")
|
||||
underside_color = Color("#CBE6FF", 0.72)
|
||||
accent_color = Color("#D9EEFF", 0.86)
|
||||
&"sky_cloudy_day":
|
||||
origins = [Vector4(-16.0, 47.0, 246.0, 129.0), Vector4(188.0, 145.0, 242.0, 127.0), Vector4(508.0, 64.0, 281.0, 147.0), Vector4(74.0, 302.0, 190.0, 100.0), Vector4(535.0, 324.0, 231.0, 121.0)]
|
||||
origins = [Vector3(0.14, 0.17, 205.0), Vector3(0.48, 0.28, 208.0), Vector3(0.83, 0.17, 226.0), Vector3(0.28, 0.45, 170.0), Vector3(0.78, 0.47, 194.0)]
|
||||
opacities = [1.0, 0.93, 0.95, 0.7, 0.69]
|
||||
outer_color = Color("#F8FCFF")
|
||||
underside_color = Color("#BED7EF", 0.9)
|
||||
accent_color = Color("#D0E5F7", 1.0)
|
||||
&"sky_sunset":
|
||||
origins = [Vector4(24.0, 82.0, 235.0, 123.0), Vector4(294.0, 244.0, 258.0, 135.0), Vector4(585.0, 310.0, 218.0, 114.0)]
|
||||
origins = [Vector3(0.18, 0.20, 210.0), Vector3(0.55, 0.37, 226.0), Vector3(0.88, 0.50, 192.0)]
|
||||
opacities = [0.85, 0.9, 0.81]
|
||||
outer_color = Color("#FFE3D1")
|
||||
underside_color = Color("#E9A8BA", 0.8)
|
||||
accent_color = Color("#FFF2D7", 0.8)
|
||||
_:
|
||||
origins = [Vector4(-4.0, 331.0, 231.0, 121.0), Vector4(314.0, 380.0, 188.0, 98.0)]
|
||||
origins = [Vector3(0.16, 0.55, 220.0), Vector3(0.68, 0.62, 178.0)]
|
||||
opacities = [0.28, 0.23]
|
||||
outer_color = Color("#A9BCE1")
|
||||
underside_color = Color("#7898C5", 0.8)
|
||||
accent_color = Color("#D7E3F7", 0.45)
|
||||
var viewport_scale: Vector2 = Vector2(size.x / 800.0, size.y / 480.0)
|
||||
for cloud_index: int in origins.size():
|
||||
var placement: Vector4 = origins[cloud_index]
|
||||
var cloud_rect: Rect2 = Rect2(
|
||||
Vector2(placement.x, placement.y) * viewport_scale,
|
||||
Vector2(placement.z, placement.w) * viewport_scale
|
||||
)
|
||||
_draw_cloud(cloud_rect, outer_color, underside_color, accent_color, opacities[cloud_index])
|
||||
var placement: Vector3 = origins[cloud_index]
|
||||
_draw_cloud(Vector2(placement.x, placement.y), placement.z, outer_color, underside_color, accent_color, opacities[cloud_index])
|
||||
|
||||
|
||||
func _draw_cloud(rect: Rect2, outer_color: Color, underside_color: Color, accent_color: Color, opacity: float) -> void:
|
||||
var outline: PackedVector2Array = _map_cloud_points(_build_cloud_outline(), rect)
|
||||
func _draw_cloud(anchor: Vector2, authored_width: float, outer_color: Color, underside_color: Color, accent_color: Color, opacity: float) -> void:
|
||||
var cloud_scale: float = _base_motif_scale() * _motif_scale * authored_width / CLOUD_VIEWBOX.x
|
||||
var center: Vector2 = _motif_anchor(anchor)
|
||||
var outline: PackedVector2Array = _map_cloud_points(_cached_cloud_outline, center, cloud_scale)
|
||||
var cloud_color: Color = _with_opacity(outer_color, opacity)
|
||||
draw_colored_polygon(outline, cloud_color)
|
||||
draw_polyline(outline, cloud_color, maxf(1.0, rect.size.y / CLOUD_VIEWBOX.y * 1.5), true)
|
||||
var underside: PackedVector2Array = _map_cloud_points(_build_cloud_underside(), rect)
|
||||
draw_polyline(outline, cloud_color, maxf(1.0, cloud_scale * 1.5), true)
|
||||
var underside: PackedVector2Array = _map_cloud_points(_cached_cloud_underside, center, cloud_scale)
|
||||
draw_colored_polygon(underside, _with_opacity(underside_color, opacity))
|
||||
for accent_curve: PackedVector2Array in _build_cloud_accents():
|
||||
var accent_points: PackedVector2Array = _map_cloud_points(_sample_curve_chain([accent_curve], 12), rect)
|
||||
draw_polyline(accent_points, _with_opacity(accent_color, opacity), maxf(1.0, rect.size.y / CLOUD_VIEWBOX.y * 3.0), true)
|
||||
for accent_points_local: PackedVector2Array in _cached_cloud_accents:
|
||||
var accent_points: PackedVector2Array = _map_cloud_points(accent_points_local, center, cloud_scale)
|
||||
draw_polyline(accent_points, _with_opacity(accent_color, opacity), maxf(1.0, cloud_scale * 3.0), true)
|
||||
|
||||
|
||||
func _draw_moon() -> void:
|
||||
var moon_curves: Array[PackedVector2Array] = _build_moon_curves()
|
||||
var moon_local: PackedVector2Array = _sample_curve_chain(moon_curves, 16)
|
||||
var outer_arc: PackedVector2Array = _sample_curve_chain(_build_moon_outer_arc_curves(), 16)
|
||||
var moon_scale: float = size.y / 480.0 * 0.63
|
||||
var moon_origin: Vector2 = Vector2(size.x * 0.7625, size.y * 0.09583)
|
||||
var moon_size: Vector2 = Vector2(150.0, 240.0) * moon_scale
|
||||
var moon_rect: Rect2 = Rect2(moon_origin, moon_size)
|
||||
var moon_outline: PackedVector2Array = _map_moon_points(moon_local, moon_rect)
|
||||
var moon_scale: float = _base_motif_scale() * _motif_scale * 0.63
|
||||
var moon_center: Vector2 = _motif_anchor(Vector2(0.75, 0.25))
|
||||
var moon_outline: PackedVector2Array = _map_moon_points(_cached_moon_outline, moon_center, moon_scale)
|
||||
draw_colored_polygon(moon_outline, Color("#17213A"))
|
||||
draw_colored_polygon(moon_outline, Color("#FFEA31"))
|
||||
var warm_rim: PackedVector2Array = _build_moon_warm_rim(outer_arc, 7.5)
|
||||
draw_colored_polygon(_map_moon_points(warm_rim, moon_rect), Color("#F4A629"))
|
||||
var highlight: PackedVector2Array = _sample_curve_chain(_build_moon_highlight(), 18)
|
||||
draw_polyline(_map_moon_points(highlight, moon_rect), Color(1.0, 0.98, 0.63, 0.62), maxf(1.0, moon_scale * 8.0), true)
|
||||
draw_colored_polygon(_map_moon_points(_cached_moon_warm_rim, moon_center, moon_scale), Color("#F4A629"))
|
||||
draw_polyline(_map_moon_points(_cached_moon_highlight, moon_center, moon_scale), Color(1.0, 0.98, 0.63, 0.62), maxf(1.0, moon_scale * 8.0), true)
|
||||
draw_polyline(moon_outline, Color("#17213A"), maxf(1.0, moon_scale * 5.5), true)
|
||||
|
||||
|
||||
func _base_motif_scale() -> float:
|
||||
return maxf(0.001, minf(size.x / BASE_VIEWPORT_SIZE.x, size.y / BASE_VIEWPORT_SIZE.y))
|
||||
|
||||
|
||||
func _motif_anchor(normalized_anchor: Vector2) -> Vector2:
|
||||
return normalized_anchor * size + _motif_shift
|
||||
|
||||
|
||||
func _build_cloud_outline() -> PackedVector2Array:
|
||||
return _sample_curve_chain(_build_cloud_outer_curves(), 10)
|
||||
|
||||
@@ -284,17 +376,17 @@ func _cubic_point(curve: PackedVector2Array, t: float) -> Vector2:
|
||||
)
|
||||
|
||||
|
||||
func _map_cloud_points(points: PackedVector2Array, rect: Rect2) -> PackedVector2Array:
|
||||
func _map_cloud_points(points: PackedVector2Array, center: Vector2, uniform_scale: float) -> PackedVector2Array:
|
||||
var mapped: PackedVector2Array = PackedVector2Array()
|
||||
for point: Vector2 in points:
|
||||
mapped.append(rect.position + Vector2(point.x / CLOUD_VIEWBOX.x * rect.size.x, point.y / CLOUD_VIEWBOX.y * rect.size.y))
|
||||
mapped.append(center + (point - CLOUD_VIEWBOX * 0.5) * uniform_scale)
|
||||
return mapped
|
||||
|
||||
|
||||
func _map_moon_points(points: PackedVector2Array, rect: Rect2) -> PackedVector2Array:
|
||||
func _map_moon_points(points: PackedVector2Array, center: Vector2, uniform_scale: float) -> PackedVector2Array:
|
||||
var mapped: PackedVector2Array = PackedVector2Array()
|
||||
for point: Vector2 in points:
|
||||
mapped.append(rect.position + point * (rect.size.y / 240.0))
|
||||
mapped.append(center + (point - Vector2(75.0, 120.0)) * uniform_scale)
|
||||
return mapped
|
||||
|
||||
|
||||
|
||||
@@ -5,15 +5,12 @@ extends SubViewportContainer
|
||||
const STAGE_SCENE_IO := preload("res://scripts/stage/stage_scene_io.gd")
|
||||
const STAGE_SCENE_ADAPTER := preload("res://scripts/editors/stage/stage_scene_adapter.gd")
|
||||
const STAGE_SPAWNER := preload("res://scripts/stage/stage_spawner.gd")
|
||||
const STAGE_SELECTION := preload("res://scripts/stage/stage_selection.gd")
|
||||
const STAGE_SKY_VIEW := preload("res://scripts/stage/scenery/stage_sky_view.gd")
|
||||
const STAGE_WALL_PANEL := preload("res://scripts/stage/scenery/stage_wall_panel.gd")
|
||||
const PROP_BLOCK := preload("res://scripts/props/prop_block.gd")
|
||||
|
||||
const PREVIEW_SIZE := Vector2i(256, 256)
|
||||
const PREVIEW_MARGIN := 18.0
|
||||
const MIN_PREVIEW_ZOOM := 0.08
|
||||
const MAX_PREVIEW_ZOOM := 0.42
|
||||
const REFERENCE_VIEWPORT_SIZE := Vector2(1280.0, 720.0)
|
||||
|
||||
var _viewport: SubViewport
|
||||
var _sky_view: StageSkyView
|
||||
@@ -82,35 +79,33 @@ func _build_preview_viewport(data: Dictionary) -> void:
|
||||
prop.gravity_scale = 0.0
|
||||
prop.collision_layer = 0
|
||||
prop.collision_mask = 0
|
||||
call_deferred("_fit_scene_to_preview")
|
||||
call_deferred("_fit_scene_to_preview", data.get("camera", {}) as Dictionary)
|
||||
|
||||
|
||||
func _fit_scene_to_preview() -> void:
|
||||
func _fit_scene_to_preview(camera_state: Dictionary) -> void:
|
||||
if _world == null or not is_instance_valid(_world):
|
||||
return
|
||||
var content_bounds := Rect2()
|
||||
var has_bounds: bool = false
|
||||
for node: Node2D in _loaded_nodes:
|
||||
if node == null or not is_instance_valid(node):
|
||||
continue
|
||||
var bounds: Rect2
|
||||
if node.get_script() == STAGE_WALL_PANEL:
|
||||
bounds = node.global_transform * (node as StageWallPanel).get_local_bounds()
|
||||
else:
|
||||
bounds = STAGE_SELECTION.get_world_aabb(node)
|
||||
bounds = bounds.abs()
|
||||
if bounds.size.x <= 0.0 or bounds.size.y <= 0.0:
|
||||
continue
|
||||
content_bounds = bounds if not has_bounds else content_bounds.merge(bounds)
|
||||
has_bounds = true
|
||||
if not has_bounds:
|
||||
_world.scale = Vector2.ONE * 0.2
|
||||
_world.position = Vector2(PREVIEW_SIZE) * 0.5
|
||||
_viewport.render_target_update_mode = SubViewport.UPDATE_ONCE
|
||||
return
|
||||
var usable_size: Vector2 = Vector2(PREVIEW_SIZE) - Vector2.ONE * PREVIEW_MARGIN * 2.0
|
||||
var fit_zoom: float = minf(usable_size.x / content_bounds.size.x, usable_size.y / content_bounds.size.y)
|
||||
fit_zoom = clampf(fit_zoom, MIN_PREVIEW_ZOOM, MAX_PREVIEW_ZOOM)
|
||||
_world.scale = Vector2.ONE * fit_zoom
|
||||
_world.position = Vector2(PREVIEW_SIZE) * 0.5 - content_bounds.get_center() * fit_zoom
|
||||
var preview_frame: Dictionary = _preview_transform_for_camera(camera_state)
|
||||
var preview_scale: float = float(preview_frame["scale"])
|
||||
_world.scale = Vector2.ONE * preview_scale
|
||||
_world.position = preview_frame["position"] as Vector2
|
||||
_viewport.render_target_update_mode = SubViewport.UPDATE_ONCE
|
||||
|
||||
|
||||
static func _preview_transform_for_camera(camera_state: Dictionary) -> Dictionary:
|
||||
var camera_position: Vector2 = STAGE_SCENE_IO.data_to_vector(camera_state.get("position"), Vector2.ZERO)
|
||||
var camera_zoom: float = float(camera_state.get("zoom", 0.5))
|
||||
if not is_finite(camera_zoom) or camera_zoom <= 0.0:
|
||||
camera_zoom = 0.5
|
||||
var square_world_extent: float = REFERENCE_VIEWPORT_SIZE.y / camera_zoom
|
||||
var focus_rect := Rect2(
|
||||
camera_position - Vector2.ONE * square_world_extent * 0.5,
|
||||
Vector2.ONE * square_world_extent
|
||||
)
|
||||
var usable_size: Vector2 = Vector2(PREVIEW_SIZE) - Vector2.ONE * PREVIEW_MARGIN * 2.0
|
||||
var preview_scale: float = minf(usable_size.x / focus_rect.size.x, usable_size.y / focus_rect.size.y)
|
||||
return {
|
||||
"focus_rect": focus_rect,
|
||||
"scale": preview_scale,
|
||||
"position": Vector2(PREVIEW_SIZE) * 0.5 - camera_position * preview_scale,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user