diff --git a/scripts/game/enemy_health_bar.gd b/scripts/game/enemy_health_bar.gd index e530319..ba26d1d 100644 --- a/scripts/game/enemy_health_bar.gd +++ b/scripts/game/enemy_health_bar.gd @@ -6,6 +6,7 @@ func _ready() -> void: enemy = get_parent() as CharacterBody2D top_level = true z_index = 30 + light_mask = 4 _update_position() func _process(_delta: float) -> void: diff --git a/scripts/game/enemy_ship.gd b/scripts/game/enemy_ship.gd index 17ff308..36e2ba8 100644 --- a/scripts/game/enemy_ship.gd +++ b/scripts/game/enemy_ship.gd @@ -56,6 +56,7 @@ func configure(player: CharacterBody2D, ship_texture: Texture2D) -> void: var visual_size := get_visual_size() _sprite.scale = Vector2.ONE * (visual_size / largest_dimension) _sprite.modulate = Color("ffc785") if loot_tier == 1 else Color.WHITE + preload("res://scripts/game/ship_shadow.gd").attach_to(_sprite) func get_visual_size() -> float: return 112.0 if loot_tier == 2 else (68.0 if loot_tier == 1 else 56.0) diff --git a/scripts/game/ship_controller.gd b/scripts/game/ship_controller.gd index 561cd1a..8892ffe 100644 --- a/scripts/game/ship_controller.gd +++ b/scripts/game/ship_controller.gd @@ -100,6 +100,7 @@ func _ready() -> void: _sprite.texture = ship_texture var largest_dimension := maxf(ship_texture.get_width(), ship_texture.get_height()) _sprite.scale = Vector2.ONE * (64.0 / largest_dimension) + preload("res://scripts/game/ship_shadow.gd").attach_to(_sprite) # Inventory children have already applied the restored equipment bonuses. hull = max_hull shields = max_shields diff --git a/scripts/game/ship_shadow.gd b/scripts/game/ship_shadow.gd new file mode 100644 index 0000000..debe26d --- /dev/null +++ b/scripts/game/ship_shadow.gd @@ -0,0 +1,54 @@ +extends RefCounted + +# Cache one simplified silhouette per texture, shared by all ships of that type. +static var _silhouettes: Dictionary = {} + +static func attach_to(sprite: Sprite2D) -> void: + if sprite.texture == null: + return + sprite.light_mask = 2 + if sprite.has_node("ShipShadow"): + return + var texture := sprite.texture + var key := texture.get_instance_id() + if not _silhouettes.has(key): + var image := texture.get_image() + if image == null or image.is_empty(): + return + if image.is_compressed(): + image.decompress() + var original_size := Vector2(image.get_size()) + var factor := minf(1.0, 128.0 / maxf(original_size.x, original_size.y)) + image.resize(maxi(1, roundi(original_size.x * factor)), maxi(1, roundi(original_size.y * factor)), Image.INTERPOLATE_BILINEAR) + var bitmap := BitMap.new() + bitmap.create_from_image_alpha(image, 0.25) + var polygons := bitmap.opaque_to_polygons(Rect2i(Vector2i.ZERO, image.get_size()), 1.5) + var silhouette := PackedVector2Array() + var largest_area := 0.0 + for polygon in polygons: + var area := _polygon_area(polygon) + if area > largest_area: + largest_area = area + silhouette = polygon + var image_scale := original_size / Vector2(image.get_size()) + for index in silhouette.size(): + silhouette[index] = silhouette[index] * image_scale - original_size * 0.5 + _silhouettes[key] = silhouette + var points: PackedVector2Array = _silhouettes[key] + if points.size() < 3: + return + var polygon := OccluderPolygon2D.new() + polygon.polygon = points + polygon.cull_mode = OccluderPolygon2D.CULL_DISABLED + var occluder := LightOccluder2D.new() + occluder.name = "ShipShadow" + occluder.occluder = polygon + occluder.occluder_light_mask = 1 + occluder.show_behind_parent = true + sprite.add_child(occluder) + +static func _polygon_area(points: PackedVector2Array) -> float: + var area := 0.0 + for index in points.size(): + area += points[index].cross(points[(index + 1) % points.size()]) + return absf(area) * 0.5 diff --git a/scripts/game/ship_shadow.gd.uid b/scripts/game/ship_shadow.gd.uid new file mode 100644 index 0000000..b65a744 --- /dev/null +++ b/scripts/game/ship_shadow.gd.uid @@ -0,0 +1 @@ +uid://b2jj0dueysvnp diff --git a/scripts/game/star_lighting.gd b/scripts/game/star_lighting.gd new file mode 100644 index 0000000..5d5b446 --- /dev/null +++ b/scripts/game/star_lighting.gd @@ -0,0 +1,53 @@ +extends RefCounted + +const LIGHT_RADIUS := 3400.0 +const LIGHT_ENERGY := 0.28 +const SHIP_LIGHT_ENERGY := 0.16 +const LIGHT_COLOR := Color(1.0, 0.93, 0.82) + +static func attach_to(star: Node2D) -> void: + var gradient := Gradient.new() + gradient.offsets = PackedFloat32Array([0.0, 0.25, 0.65, 1.0]) + gradient.colors = PackedColorArray([Color.WHITE, Color(1, 1, 1, 0.9), Color(1, 1, 1, 0.5), Color(1, 1, 1, 0)]) + var texture := GradientTexture2D.new() + texture.gradient = gradient + texture.width = 512 + texture.height = 512 + texture.fill = GradientTexture2D.FILL_RADIAL + texture.fill_from = Vector2(0.5, 0.5) + texture.fill_to = Vector2(0.5, 0.0) + + var light := PointLight2D.new() + light.name = "StarLight" + light.texture = texture + light.texture_scale = LIGHT_RADIUS * 2.0 / texture.width + light.color = LIGHT_COLOR + light.energy = LIGHT_ENERGY + light.range_item_cull_mask = 1 + light.shadow_enabled = true + light.shadow_item_cull_mask = 1 + light.shadow_filter = Light2D.SHADOW_FILTER_PCF5 + light.shadow_filter_smooth = 2.0 + star.add_child(light) + + # A separate mask lights hulls without their own silhouettes darkening them. + var hull_light := PointLight2D.new() + hull_light.name = "HullLight" + hull_light.texture = texture + hull_light.texture_scale = light.texture_scale + hull_light.color = LIGHT_COLOR + hull_light.energy = SHIP_LIGHT_ENERGY + hull_light.range_item_cull_mask = 2 + star.add_child(hull_light) + + var glow := Sprite2D.new() + glow.name = "StarGlow" + glow.texture = texture + glow.scale = Vector2.ONE * (float(star.get("radius")) * 5.0 / texture.width) + glow.modulate = Color(LIGHT_COLOR, 0.09) + glow.z_index = -1 + var material := CanvasItemMaterial.new() + material.blend_mode = CanvasItemMaterial.BLEND_MODE_ADD + material.light_mode = CanvasItemMaterial.LIGHT_MODE_UNSHADED + glow.material = material + star.add_child(glow) diff --git a/scripts/game/star_lighting.gd.uid b/scripts/game/star_lighting.gd.uid new file mode 100644 index 0000000..ead6262 --- /dev/null +++ b/scripts/game/star_lighting.gd.uid @@ -0,0 +1 @@ +uid://kmnxv1hp273t diff --git a/scripts/levels/celestial_body.gd b/scripts/levels/celestial_body.gd index 62d211f..a814399 100644 --- a/scripts/levels/celestial_body.gd +++ b/scripts/levels/celestial_body.gd @@ -14,8 +14,15 @@ func _ready() -> void: sprite.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR var longest_side := maxf(body_texture.get_width(), body_texture.get_height()) sprite.scale = Vector2.ONE * (radius * 2.0 / longest_side) + if is_star: + var material := CanvasItemMaterial.new() + material.light_mode = CanvasItemMaterial.LIGHT_MODE_UNSHADED + sprite.material = material add_child(sprite) + if is_star: + preload("res://scripts/game/star_lighting.gd").attach_to(self) _label = Label.new() + _label.light_mask = 4 _label.position = Vector2(-160, radius + 14) _label.size = Vector2(320, 54) _label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER diff --git a/tests/levels/system_lifecycle_test.gd b/tests/levels/system_lifecycle_test.gd index 3659955..60424cf 100644 --- a/tests/levels/system_lifecycle_test.gd +++ b/tests/levels/system_lifecycle_test.gd @@ -13,6 +13,12 @@ func _ready() -> void: get_tree().current_scene = game _freeze(game) _assert_boss_and_health(game) + var star := game.get_node("CelestialBodies/CentralStar") + var star_light: PointLight2D = star.get_node("StarLight") + assert(star_light.shadow_enabled and star_light.energy < 0.5) + assert(star_light.position == Vector2.ZERO) + assert(star_light.shadow_filter == Light2D.SHADOW_FILTER_PCF5) + assert(star.get_node("HullLight").get("range_item_cull_mask") == 2) assert(GameState.system_level == 1 and int(game.call("get_remaining_enemies")) == 5) assert(not game.has_node("WeaponTestTarget")) assert(game.get_node("CelestialBodies").get_child_count() >= 2) @@ -146,10 +152,14 @@ func _freeze(game: Node2D) -> void: game.set_physics_process(false) game.get_node("PlayerShip").set_physics_process(false) game.get_node("PlayerShip/WeaponController").set_physics_process(false) + assert(game.get_node("PlayerShip/Sprite2D/ShipShadow") is LightOccluder2D) for enemy in get_tree().get_nodes_in_group("enemies"): if game.is_ancestor_of(enemy): var data: Dictionary = game.get("system_data") assert(enemy.get_node("Sprite2D").get("texture") == game.ENEMY_TEXTURES[int(data["enemy_texture_index"])]) + var shadow: LightOccluder2D = enemy.get_node("Sprite2D/ShipShadow") + assert(shadow.occluder.polygon.size() >= 3) + assert(shadow.global_scale.is_equal_approx(enemy.get_node("Sprite2D").global_scale)) enemy.set_physics_process(false) func _assert_boss_and_health(game: Node2D) -> void: