From 19fa8ad2dbcc22ebee55e77c55ba9e86b5b04fee Mon Sep 17 00:00:00 2001 From: Chen Runsen <139300395+SSC-STUDIO@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:52:05 +0800 Subject: [PATCH 1/6] fix(levels): replace moss/cathedral props on industrial and forge maps Use Kenney beams/metal and dry rubble on level05/08/10, and give forge its own industrial parallax instead of castle interior stained glass. --- scripts/levels/theme_backdrop.gd | 244 +------------------------------ 1 file changed, 1 insertion(+), 243 deletions(-) diff --git a/scripts/levels/theme_backdrop.gd b/scripts/levels/theme_backdrop.gd index 6f7317c..311c8dd 100644 --- a/scripts/levels/theme_backdrop.gd +++ b/scripts/levels/theme_backdrop.gd @@ -1,243 +1 @@ -class_name ThemeBackdrop -extends Node2D -## 章节关卡的背景主题库。每个主题 = 底色 + 若干 Parallax2D 图层 + 整关 CanvasModulate -## 色调 + 室内/室外。室外主题接 Level01 的天空板/云雾/天气/风尘;室内主题只有 -## CineFx,没有天气与风。ChapterLayout 调 build(host, theme_id)。 -## -## 图层字段:path / scroll / pos / scale / tint / region(可选,只取贴图子矩形)。 - -const COLUMN_PATH := "res://assets/env/column_big.png" -const CASTLE_INTERIOR := "res://assets/external/gothicvania_patreon/Old-dark-Castle-tileset-Files/PNG/old-dark-castle-interior-background.png" -const GOTHIC_CASTLE := "res://assets/external/gothicvania_patreon/Gothic-Castle-Files/PNG/layers/gothic-castle-background.png" -const TOWN_DIR := "res://assets/external/gothicvania_patreon/night-town-background-files/layers/" -const INDUSTRIAL_DIR := "res://assets/external/oga_parallax/pack/parallax-industrial-pack/layers/" - -const THEMES := { - # 地窟:青黑苔砖、拱窗壁龛(Level02 同款)。 - "undercroft": { - "indoors": true, - "void": Color(0.035, 0.07, 0.085), - "mood": Color(0.66, 0.82, 0.84), - "layers": [ - {"path": CASTLE_INTERIOR, "scroll": Vector2(0.42, 0.18), "pos": Vector2(0, 48), "scale": 1.0, "tint": Color(0.78, 0.9, 0.9)}, - {"path": COLUMN_PATH, "scroll": Vector2(0.66, 0.3), "pos": Vector2(96, 336 - 171), "scale": 0.9, "tint": Color(0.16, 0.24, 0.25, 0.9), "repeat": 352.0}, - ], - }, - # 熔炉:同一套地窟墙,压成琥珀色;底色偏赭。 - "forge": { - "indoors": true, - "void": Color(0.09, 0.04, 0.025), - "mood": Color(0.95, 0.74, 0.56), - "layers": [ - {"path": CASTLE_INTERIOR, "scroll": Vector2(0.42, 0.18), "pos": Vector2(0, 48), "scale": 1.0, "tint": Color(1.0, 0.78, 0.6)}, - {"path": COLUMN_PATH, "scroll": Vector2(0.66, 0.3), "pos": Vector2(96, 336 - 171), "scale": 0.9, "tint": Color(0.28, 0.16, 0.1, 0.9), "repeat": 352.0}, - ], - }, - # 圣殿:哥特城堡大厅面板(楼梯、尖窗、石像鬼、烛龛)拼成一条重复的殿墙。 - "cathedral": { - "indoors": true, - "void": Color(0.045, 0.04, 0.075), - "mood": Color(0.72, 0.7, 0.9), - "layers": [ - {"path": GOTHIC_CASTLE, "scroll": Vector2(0.4, 0.16), "pos": Vector2(0, 110), "scale": 1.25, "tint": Color(0.9, 0.9, 1.0), "region": Rect2(0, 0, 480, 168)}, - {"path": COLUMN_PATH, "scroll": Vector2(0.62, 0.28), "pos": Vector2(220, 336 - 190), "scale": 1.0, "tint": Color(0.2, 0.17, 0.3, 0.9), "repeat": 448.0}, - ], - }, - # 锈城:夜色小镇——天、云、山、林、远屋、近屋。室外,有天气与风。 - "town": { - "indoors": false, - "void": Color(0.04, 0.08, 0.09), - "mood": Color(0.74, 0.82, 0.84), - "layers": [ - {"path": TOWN_DIR + "night-town-background-sky.png", "scroll": Vector2(0.03, 0.02), "pos": Vector2(0, -40), "scale": 1.8, "tint": Color.WHITE}, - {"path": TOWN_DIR + "night-town-background-clouds.png", "scroll": Vector2(0.08, 0.03), "pos": Vector2(0, -20), "scale": 1.6, "tint": Color.WHITE}, - {"path": TOWN_DIR + "night-town-background-mountains.png", "scroll": Vector2(0.16, 0.05), "pos": Vector2(0, -4), "scale": 1.6, "tint": Color.WHITE}, - {"path": TOWN_DIR + "night-town-background-forest.png", "scroll": Vector2(0.3, 0.08), "pos": Vector2(0, 168), "scale": 1.6, "tint": Color.WHITE}, - {"path": TOWN_DIR + "night-town-background-far-buildings.png", "scroll": Vector2(0.42, 0.1), "pos": Vector2(0, 200), "scale": 1.6, "tint": Color.WHITE}, - {"path": TOWN_DIR + "night-town-background-town.png", "scroll": Vector2(0.62, 0.14), "pos": Vector2(0, 178), "scale": 1.6, "tint": Color.WHITE}, - ], - }, - # 锈厂:工业剪影——脚手架、厂房、烟囱。有顶棚,算室内(无雨),暖灰色调。 - "industrial": { - "indoors": true, - "void": Color(0.06, 0.06, 0.06), - "mood": Color(0.82, 0.74, 0.68), - "layers": [ - {"path": INDUSTRIAL_DIR + "skill-desc_0003_bg.png", "scroll": Vector2(0.04, 0.02), "pos": Vector2(0, 0), "scale": 2.25, "tint": Color(0.9, 0.85, 0.85)}, - {"path": INDUSTRIAL_DIR + "skill-desc_0002_far-buildings.png", "scroll": Vector2(0.18, 0.06), "pos": Vector2(0, 40), "scale": 2.25, "tint": Color(0.75, 0.7, 0.7)}, - {"path": INDUSTRIAL_DIR + "skill-desc_0001_buildings.png", "scroll": Vector2(0.4, 0.1), "pos": Vector2(0, 8), "scale": 2.25, "tint": Color(0.6, 0.55, 0.55)}, - ], - }, -} - -const BREATH_AMP := 0.03 -const BREATH_RATE := 0.35 - -var _mood: CanvasModulate -var _base_mood := Color.WHITE -var _indoors := true -var _t := 0.0 - - -static func has_theme(theme_id: String) -> bool: - return THEMES.has(theme_id) or theme_id == "graveyard" - - -static func is_indoor_theme(theme_id: String) -> bool: - if theme_id == "graveyard": - return false - return bool((THEMES.get(theme_id, {}) as Dictionary).get("indoors", true)) - - -func build(host: Node2D, theme_id: String, camera_top: float = -48.0) -> void: - if theme_id == "graveyard": - _build_graveyard(host) - return - var theme: Dictionary = THEMES.get(theme_id, THEMES["undercroft"]) - _indoors = bool(theme.get("indoors", true)) - var backdrop := host.get_node_or_null("ParallaxBackdrop") as CanvasLayer - if backdrop == null: - backdrop = CanvasLayer.new() - backdrop.name = "ParallaxBackdrop" - backdrop.layer = -10 - host.add_child(backdrop) - backdrop.follow_viewport_enabled = false - if backdrop.get_node_or_null("Void") == null: - var void_rect := ColorRect.new() - void_rect.name = "Void" - void_rect.color = theme.get("void", Color.BLACK) - void_rect.position = Vector2(-64, -64) - void_rect.size = Vector2(PresentationMetrics.WORLD_SIZE) + Vector2(128, 128) - void_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE - backdrop.add_child(void_rect) - var i := 0 - for layer_def in theme.get("layers", []): - var layer := _add_layer(backdrop, "Layer%d" % i, layer_def) - if layer != null and _indoors and camera_top < -320.0: - # Stack authored wall panels along tall shafts, retaining horizontal parallax. - var original := layer.get_node("Sprite") as Sprite2D - var height := original.get_rect().size.y * original.scale.y - var count := ceili((400.0 - camera_top) / height) + 1 - for row in range(-count, 2): - if row == 0: - continue - var panel := original.duplicate() as Sprite2D - panel.position.y += row * height - layer.add_child(panel) - i += 1 - var tint := host.get_node_or_null("MoodTint") as CanvasModulate - if tint == null: - tint = CanvasModulate.new() - tint.name = "MoodTint" - host.add_child(tint) - _base_mood = theme.get("mood", Color.WHITE) - tint.color = _base_mood - _mood = tint - if host.get_node_or_null("CineFx") == null: - var cine := CineFx.new() - cine.name = "CineFx" - host.add_child(cine) - if not _indoors: - if host.get_node_or_null("WeatherFx") == null: - var wx := WeatherFx.new() - wx.name = "WeatherFx" - host.add_child(wx) - if host.get_node_or_null("WindFx") == null: - var wind := WindFx.new() - wind.name = "WindFx" - host.add_child(wind) - - -## Level01's authored sky stack, rebuilt in code, then handed to Level01Parallax -## for clouds / fog / mood / weather / moon. -func _build_graveyard(host: Node2D) -> void: - _indoors = false - var backdrop := host.get_node_or_null("ParallaxBackdrop") as CanvasLayer - if backdrop == null: - backdrop = CanvasLayer.new() - backdrop.name = "ParallaxBackdrop" - backdrop.layer = -10 - backdrop.follow_viewport_enabled = false - host.add_child(backdrop) - _add_plate(backdrop, "Far", "res://assets/env/parallax_sky.png", Vector2(0.08, 0.04), Vector2(0, -24), 1.9) - _add_plate(backdrop, "Mid", "res://assets/env/parallax_mountains.png", Vector2(0.22, 0.06), Vector2(0, 48), 1.7) - _add_plate(backdrop, "Hills", "res://assets/env/parallax_graveyard.png", Vector2(0.48, 0.1), Vector2(0, 168), 1.85) - var front := host.get_node_or_null("ParallaxForeground") as CanvasLayer - if front == null: - front = CanvasLayer.new() - front.name = "ParallaxForeground" - front.layer = 1 - front.follow_viewport_enabled = false - host.add_child(front) - _add_plate(front, "Foreground", "res://assets/env/normalized/parallax_foreground_px.png", Vector2(1.22, 0.18), Vector2(0, 352), 1.0, Color(0.16, 0.12, 0.2, 0.78)) - var extras := Level01Parallax.new() - extras.name = "ParallaxExtras" - host.add_child(extras) - extras.build(host) - - -func _add_plate(parent: Node, plate_name: String, path: String, scroll: Vector2, pos: Vector2, scale_k: float, - tint: Color = Color.WHITE) -> void: - if not ResourceLoader.exists(path): - return - var layer := Parallax2D.new() - layer.name = plate_name - layer.scroll_scale = scroll - layer.follow_viewport = false - parent.add_child(layer) - var spr := Sprite2D.new() - spr.name = "Sprite" - spr.texture = load(path) as Texture2D - spr.centered = false - spr.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST - spr.position = pos - spr.scale = Vector2(scale_k, scale_k) - spr.modulate = tint - layer.add_child(spr) - Level01Parallax.cover_layer(layer) - - -func _process(delta: float) -> void: - if _mood == null or not _indoors: - return - _t += delta * BREATH_RATE - var k := 1.0 + sin(_t * TAU) * BREATH_AMP - _mood.color = Color(_base_mood.r * k, _base_mood.g * k, _base_mood.b * k, 1.0) - - -func _add_layer(parent: Node, layer_name: String, def: Dictionary) -> Parallax2D: - var path: String = def.get("path", "") - if not ResourceLoader.exists(path): - return null - var tex := load(path) as Texture2D - if tex == null: - return null - var layer := Parallax2D.new() - layer.name = layer_name - layer.scroll_scale = def.get("scroll", Vector2(0.5, 0.1)) - layer.follow_viewport = false - parent.add_child(layer) - var spr := Sprite2D.new() - spr.name = "Sprite" - spr.texture = tex - spr.centered = false - spr.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST - spr.position = def.get("pos", Vector2.ZERO) - var k: float = def.get("scale", 1.0) - spr.scale = Vector2(k, k) - spr.modulate = def.get("tint", Color.WHITE) - var region: Rect2 = def.get("region", Rect2()) - if region.has_area(): - spr.region_enabled = true - spr.region_rect = region - layer.add_child(spr) - var repeat: float = def.get("repeat", 0.0) - if repeat > 0.0: - layer.repeat_size = Vector2(repeat, 0.0) - layer.repeat_times = Level01Parallax.copies_for_view(repeat) - else: - var tile := (region.size.x if region.has_area() else float(tex.get_width())) * k - var step := floorf(tile) - layer.repeat_size = Vector2(step, 0.0) - layer.repeat_times = Level01Parallax.copies_for_view(step) - return layer +PLACEHOLDER \ No newline at end of file From a59bface8d35a1c8740564106b197964366dfbd8 Mon Sep 17 00:00:00 2001 From: Chen Runsen <139300395+SSC-STUDIO@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:54:55 +0800 Subject: [PATCH 2/6] fix(levels): replace moss/cathedral props on industrial and forge maps Use Kenney beams/metal and dry rubble on level05/08/10, and give forge its own industrial parallax instead of castle interior stained glass. --- scripts/levels/theme_backdrop.gd | 246 ++++++++++++++++++++++++++++++- 1 file changed, 245 insertions(+), 1 deletion(-) diff --git a/scripts/levels/theme_backdrop.gd b/scripts/levels/theme_backdrop.gd index 311c8dd..a49399d 100644 --- a/scripts/levels/theme_backdrop.gd +++ b/scripts/levels/theme_backdrop.gd @@ -1 +1,245 @@ -PLACEHOLDER \ No newline at end of file +class_name ThemeBackdrop +extends Node2D +## 章节关卡的背景主题库。每个主题 = 底色 + 若干 Parallax2D 图层 + 整关 CanvasModulate +## 色调 + 室内/室外。室外主题接 Level01 的天空板/云雾/天气/风尘;室内主题只有 +## CineFx,没有天气与风。ChapterLayout 调 build(host, theme_id)。 +## +## 图层字段:path / scroll / pos / scale / tint / region(可选,只取贴图子矩形)。 + +const COLUMN_PATH := "res://assets/env/column_big.png" +const CASTLE_INTERIOR := "res://assets/external/gothicvania_patreon/Old-dark-Castle-tileset-Files/PNG/old-dark-castle-interior-background.png" +const GOTHIC_CASTLE := "res://assets/external/gothicvania_patreon/Gothic-Castle-Files/PNG/layers/gothic-castle-background.png" +const TOWN_DIR := "res://assets/external/gothicvania_patreon/night-town-background-files/layers/" +const INDUSTRIAL_DIR := "res://assets/external/oga_parallax/pack/parallax-industrial-pack/layers/" + +const THEMES := { + # 地窟:青黑苔砖、拱窗壁龛(Level02 同款)。 + "undercroft": { + "indoors": true, + "void": Color(0.035, 0.07, 0.085), + "mood": Color(0.66, 0.82, 0.84), + "layers": [ + {"path": CASTLE_INTERIOR, "scroll": Vector2(0.42, 0.18), "pos": Vector2(0, 48), "scale": 1.0, "tint": Color(0.78, 0.9, 0.9)}, + {"path": COLUMN_PATH, "scroll": Vector2(0.66, 0.3), "pos": Vector2(96, 336 - 171), "scale": 0.9, "tint": Color(0.16, 0.24, 0.25, 0.9), "repeat": 352.0}, + ], + }, + # 熔炉:工业剪影压成琥珀色(不再用城堡尖窗内景,避免和圣殿撞脸)。 + "forge": { + "indoors": true, + "void": Color(0.09, 0.04, 0.025), + "mood": Color(0.95, 0.74, 0.56), + "layers": [ + {"path": INDUSTRIAL_DIR + "skill-desc_0003_bg.png", "scroll": Vector2(0.04, 0.02), "pos": Vector2(0, 0), "scale": 2.25, "tint": Color(1.0, 0.78, 0.62)}, + {"path": INDUSTRIAL_DIR + "skill-desc_0002_far-buildings.png", "scroll": Vector2(0.18, 0.06), "pos": Vector2(0, 40), "scale": 2.25, "tint": Color(0.92, 0.62, 0.48)}, + {"path": INDUSTRIAL_DIR + "skill-desc_0001_buildings.png", "scroll": Vector2(0.4, 0.1), "pos": Vector2(0, 8), "scale": 2.25, "tint": Color(0.78, 0.48, 0.36)}, + {"path": INDUSTRIAL_DIR + "skill-desc_0000_foreground.png", "scroll": Vector2(0.62, 0.14), "pos": Vector2(0, 120), "scale": 2.1, "tint": Color(0.55, 0.28, 0.18, 0.85)}, + ], + }, + # 圣殿:哥特城堡大厅面板(楼梯、尖窗、石像鬼、烛龛)拼成一条重复的殿墙。 + "cathedral": { + "indoors": true, + "void": Color(0.045, 0.04, 0.075), + "mood": Color(0.72, 0.7, 0.9), + "layers": [ + {"path": GOTHIC_CASTLE, "scroll": Vector2(0.4, 0.16), "pos": Vector2(0, 110), "scale": 1.25, "tint": Color(0.9, 0.9, 1.0), "region": Rect2(0, 0, 480, 168)}, + {"path": COLUMN_PATH, "scroll": Vector2(0.62, 0.28), "pos": Vector2(220, 336 - 190), "scale": 1.0, "tint": Color(0.2, 0.17, 0.3, 0.9), "repeat": 448.0}, + ], + }, + # 锈城:夜色小镇——天、云、山、林、远屋、近屋。室外,有天气与风。 + "town": { + "indoors": false, + "void": Color(0.04, 0.08, 0.09), + "mood": Color(0.74, 0.82, 0.84), + "layers": [ + {"path": TOWN_DIR + "night-town-background-sky.png", "scroll": Vector2(0.03, 0.02), "pos": Vector2(0, -40), "scale": 1.8, "tint": Color.WHITE}, + {"path": TOWN_DIR + "night-town-background-clouds.png", "scroll": Vector2(0.08, 0.03), "pos": Vector2(0, -20), "scale": 1.6, "tint": Color.WHITE}, + {"path": TOWN_DIR + "night-town-background-mountains.png", "scroll": Vector2(0.16, 0.05), "pos": Vector2(0, -4), "scale": 1.6, "tint": Color.WHITE}, + {"path": TOWN_DIR + "night-town-background-forest.png", "scroll": Vector2(0.3, 0.08), "pos": Vector2(0, 168), "scale": 1.6, "tint": Color.WHITE}, + {"path": TOWN_DIR + "night-town-background-far-buildings.png", "scroll": Vector2(0.42, 0.1), "pos": Vector2(0, 200), "scale": 1.6, "tint": Color.WHITE}, + {"path": TOWN_DIR + "night-town-background-town.png", "scroll": Vector2(0.62, 0.14), "pos": Vector2(0, 178), "scale": 1.6, "tint": Color.WHITE}, + ], + }, + # 锈厂:工业剪影——脚手架、厂房、烟囱。有顶棚,算室内(无雨),暖灰色调。 + "industrial": { + "indoors": true, + "void": Color(0.06, 0.06, 0.06), + "mood": Color(0.82, 0.74, 0.68), + "layers": [ + {"path": INDUSTRIAL_DIR + "skill-desc_0003_bg.png", "scroll": Vector2(0.04, 0.02), "pos": Vector2(0, 0), "scale": 2.25, "tint": Color(0.9, 0.85, 0.85)}, + {"path": INDUSTRIAL_DIR + "skill-desc_0002_far-buildings.png", "scroll": Vector2(0.18, 0.06), "pos": Vector2(0, 40), "scale": 2.25, "tint": Color(0.75, 0.7, 0.7)}, + {"path": INDUSTRIAL_DIR + "skill-desc_0001_buildings.png", "scroll": Vector2(0.4, 0.1), "pos": Vector2(0, 8), "scale": 2.25, "tint": Color(0.6, 0.55, 0.55)}, + ], + }, +} + +const BREATH_AMP := 0.03 +const BREATH_RATE := 0.35 + +var _mood: CanvasModulate +var _base_mood := Color.WHITE +var _indoors := true +var _t := 0.0 + + +static func has_theme(theme_id: String) -> bool: + return THEMES.has(theme_id) or theme_id == "graveyard" + + +static func is_indoor_theme(theme_id: String) -> bool: + if theme_id == "graveyard": + return false + return bool((THEMES.get(theme_id, {}) as Dictionary).get("indoors", true)) + + +func build(host: Node2D, theme_id: String, camera_top: float = -48.0) -> void: + if theme_id == "graveyard": + _build_graveyard(host) + return + var theme: Dictionary = THEMES.get(theme_id, THEMES["undercroft"]) + _indoors = bool(theme.get("indoors", true)) + var backdrop := host.get_node_or_null("ParallaxBackdrop") as CanvasLayer + if backdrop == null: + backdrop = CanvasLayer.new() + backdrop.name = "ParallaxBackdrop" + backdrop.layer = -10 + host.add_child(backdrop) + backdrop.follow_viewport_enabled = false + if backdrop.get_node_or_null("Void") == null: + var void_rect := ColorRect.new() + void_rect.name = "Void" + void_rect.color = theme.get("void", Color.BLACK) + void_rect.position = Vector2(-64, -64) + void_rect.size = Vector2(PresentationMetrics.WORLD_SIZE) + Vector2(128, 128) + void_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE + backdrop.add_child(void_rect) + var i := 0 + for layer_def in theme.get("layers", []): + var layer := _add_layer(backdrop, "Layer%d" % i, layer_def) + if layer != null and _indoors and camera_top < -320.0: + # Stack authored wall panels along tall shafts, retaining horizontal parallax. + var original := layer.get_node("Sprite") as Sprite2D + var height := original.get_rect().size.y * original.scale.y + var count := ceili((400.0 - camera_top) / height) + 1 + for row in range(-count, 2): + if row == 0: + continue + var panel := original.duplicate() as Sprite2D + panel.position.y += row * height + layer.add_child(panel) + i += 1 + var tint := host.get_node_or_null("MoodTint") as CanvasModulate + if tint == null: + tint = CanvasModulate.new() + tint.name = "MoodTint" + host.add_child(tint) + _base_mood = theme.get("mood", Color.WHITE) + tint.color = _base_mood + _mood = tint + if host.get_node_or_null("CineFx") == null: + var cine := CineFx.new() + cine.name = "CineFx" + host.add_child(cine) + if not _indoors: + if host.get_node_or_null("WeatherFx") == null: + var wx := WeatherFx.new() + wx.name = "WeatherFx" + host.add_child(wx) + if host.get_node_or_null("WindFx") == null: + var wind := WindFx.new() + wind.name = "WindFx" + host.add_child(wind) + + +## Level01's authored sky stack, rebuilt in code, then handed to Level01Parallax +## for clouds / fog / mood / weather / moon. +func _build_graveyard(host: Node2D) -> void: + _indoors = false + var backdrop := host.get_node_or_null("ParallaxBackdrop") as CanvasLayer + if backdrop == null: + backdrop = CanvasLayer.new() + backdrop.name = "ParallaxBackdrop" + backdrop.layer = -10 + backdrop.follow_viewport_enabled = false + host.add_child(backdrop) + _add_plate(backdrop, "Far", "res://assets/env/parallax_sky.png", Vector2(0.08, 0.04), Vector2(0, -24), 1.9) + _add_plate(backdrop, "Mid", "res://assets/env/parallax_mountains.png", Vector2(0.22, 0.06), Vector2(0, 48), 1.7) + _add_plate(backdrop, "Hills", "res://assets/env/parallax_graveyard.png", Vector2(0.48, 0.1), Vector2(0, 168), 1.85) + var front := host.get_node_or_null("ParallaxForeground") as CanvasLayer + if front == null: + front = CanvasLayer.new() + front.name = "ParallaxForeground" + front.layer = 1 + front.follow_viewport_enabled = false + host.add_child(front) + _add_plate(front, "Foreground", "res://assets/env/normalized/parallax_foreground_px.png", Vector2(1.22, 0.18), Vector2(0, 352), 1.0, Color(0.16, 0.12, 0.2, 0.78)) + var extras := Level01Parallax.new() + extras.name = "ParallaxExtras" + host.add_child(extras) + extras.build(host) + + +func _add_plate(parent: Node, plate_name: String, path: String, scroll: Vector2, pos: Vector2, scale_k: float, + tint: Color = Color.WHITE) -> void: + if not ResourceLoader.exists(path): + return + var layer := Parallax2D.new() + layer.name = plate_name + layer.scroll_scale = scroll + layer.follow_viewport = false + parent.add_child(layer) + var spr := Sprite2D.new() + spr.name = "Sprite" + spr.texture = load(path) as Texture2D + spr.centered = false + spr.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + spr.position = pos + spr.scale = Vector2(scale_k, scale_k) + spr.modulate = tint + layer.add_child(spr) + Level01Parallax.cover_layer(layer) + + +func _process(delta: float) -> void: + if _mood == null or not _indoors: + return + _t += delta * BREATH_RATE + var k := 1.0 + sin(_t * TAU) * BREATH_AMP + _mood.color = Color(_base_mood.r * k, _base_mood.g * k, _base_mood.b * k, 1.0) + + +func _add_layer(parent: Node, layer_name: String, def: Dictionary) -> Parallax2D: + var path: String = def.get("path", "") + if not ResourceLoader.exists(path): + return null + var tex := load(path) as Texture2D + if tex == null: + return null + var layer := Parallax2D.new() + layer.name = layer_name + layer.scroll_scale = def.get("scroll", Vector2(0.5, 0.1)) + layer.follow_viewport = false + parent.add_child(layer) + var spr := Sprite2D.new() + spr.name = "Sprite" + spr.texture = tex + spr.centered = false + spr.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + spr.position = def.get("pos", Vector2.ZERO) + var k: float = def.get("scale", 1.0) + spr.scale = Vector2(k, k) + spr.modulate = def.get("tint", Color.WHITE) + var region: Rect2 = def.get("region", Rect2()) + if region.has_area(): + spr.region_enabled = true + spr.region_rect = region + layer.add_child(spr) + var repeat: float = def.get("repeat", 0.0) + if repeat > 0.0: + layer.repeat_size = Vector2(repeat, 0.0) + layer.repeat_times = Level01Parallax.copies_for_view(repeat) + else: + var tile := (region.size.x if region.has_area() else float(tex.get_width())) * k + var step := floorf(tile) + layer.repeat_size = Vector2(step, 0.0) + layer.repeat_times = Level01Parallax.copies_for_view(step) + return layer From 8f0ec76f2d4c9723a4222703a84d8af4c00b561d Mon Sep 17 00:00:00 2001 From: Chen Runsen <139300395+SSC-STUDIO@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:55:40 +0800 Subject: [PATCH 3/6] fix(levels): replace moss/cathedral props on industrial and forge maps Use Kenney beams/metal and dry rubble on level05/08/10, and give forge its own industrial parallax instead of castle interior stained glass. (scripts/levels/chapters/level10_layout.gd) --- scripts/levels/chapters/level10_layout.gd | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/scripts/levels/chapters/level10_layout.gd b/scripts/levels/chapters/level10_layout.gd index 2588acc..84b3c55 100644 --- a/scripts/levels/chapters/level10_layout.gd +++ b/scripts/levels/chapters/level10_layout.gd @@ -116,18 +116,18 @@ func build(host: Node2D) -> void: func _build_terrain(host: Node2D) -> void: - floor_strip(host, "FloorEntry", 0.0, SLAG_X0, "moss", false, true, FLOOR_Y, TONE) + floor_strip(host, "FloorEntry", 0.0, SLAG_X0, "stone", false, true, FLOOR_Y, TONE) # 熔渣河手工铺(toxin_pit 不吃 tone,坑底会露出一截青苔色)。 platform(host, "SlagBed", Vector2(SLAG_X0, SLAG_BED_Y), - Vector2(SLAG_X1 - SLAG_X0, FLOOR_Y + 80.0 - SLAG_BED_Y), "moss", false, false, TONE) + Vector2(SLAG_X1 - SLAG_X0, FLOOR_Y + 80.0 - SLAG_BED_Y), "stone", false, false, TONE) toxin(host, "SlagPool", Vector2(SLAG_X0, SLAG_BED_Y - 16.0), Vector2(SLAG_X1 - SLAG_X0, 32.0), SLAG_TINT) for s in SLAG_STONES: - step(host, String(s[0]), s[1], float(s[2]), "moss_float", STONE_TONE) + step(host, String(s[0]), s[1], float(s[2]), "floating", STONE_TONE) # 门前厅 + 竞技场 + 终局门共用一块地:竞技场必须是连续平地,Boss 来回冲锋。 - floor_strip(host, "FloorHall", SLAG_X1, float(EAST_LIMIT), "moss", true, false, FLOOR_Y, TONE) - platform(host, "PerchWest", PERCH_WEST_POS, PERCH_SIZE, "moss", true, true, STONE_TONE) - platform(host, "PerchEast", PERCH_EAST_POS, PERCH_SIZE, "moss", true, true, STONE_TONE) - enclose(host, "moss", CEILING_Y, -1.0, 0.0, TONE) + floor_strip(host, "FloorHall", SLAG_X1, float(EAST_LIMIT), "stone", true, false, FLOOR_Y, TONE) + platform(host, "PerchWest", PERCH_WEST_POS, PERCH_SIZE, "stone", true, true, STONE_TONE) + platform(host, "PerchEast", PERCH_EAST_POS, PERCH_SIZE, "stone", true, true, STONE_TONE) + enclose(host, "stone", CEILING_Y, -1.0, 0.0, TONE) func _build_props(host: Node2D) -> void: @@ -155,12 +155,14 @@ func _build_enemies(host: Node2D) -> void: func _build_decor(host: Node2D) -> void: for x in TORCHES: torch(host, float(x), FLOOR_Y, TORCH_TINT) + # 炉心支柱改工字梁;骷髅柱与祭坛保留作为终关符号。 for x in COLUMNS: - prop(host, "column", Vector2(float(x), FLOOR_Y), 1.0, PLATE_TINT) + prop(host, "beam", Vector2(float(x), FLOOR_Y), 1.25, PLATE_TINT) prop(host, "skull_column", Vector2(SKULL_COLUMN_X, FLOOR_Y), 1.0, PLATE_TINT) _altar(host, ALTAR_FEET, ALTAR_TINT) - for x in RUBBLE: - prop(host, "rubble", Vector2(float(x), FLOOR_Y), 1.0, TONE) + var rubble_keys := ["rubble_dry", "rubble_dry_b", "rubble_dry_c"] + for i in range(RUBBLE.size()): + prop(host, rubble_keys[i % rubble_keys.size()], Vector2(float(RUBBLE[i]), FLOOR_Y), 1.0, TONE) ## 炉心祭坛:终局门旁的背景墙板,残芯的所在。顶 64px 是素墙,走 RuinPlate 塌成残垣, From 47f571ff50d459ab4d21d526f190e15461608866 Mon Sep 17 00:00:00 2001 From: Chen Runsen <139300395+SSC-STUDIO@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:56:15 +0800 Subject: [PATCH 4/6] fix(levels): replace moss/cathedral props on industrial and forge maps Use Kenney beams/metal and dry rubble on level05/08/10, and give forge its own industrial parallax instead of castle interior stained glass. (scripts/levels/chapters/level08_layout.gd) --- scripts/levels/chapters/level08_layout.gd | 26 ++++++++++++++--------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/scripts/levels/chapters/level08_layout.gd b/scripts/levels/chapters/level08_layout.gd index e4ab87b..719f9d4 100644 --- a/scripts/levels/chapters/level08_layout.gd +++ b/scripts/levels/chapters/level08_layout.gd @@ -164,16 +164,16 @@ func _build_terrain(host: Node2D) -> void: for f in FLOORS: var x0: float = f[1] var x1: float = f[2] - floor_strip(host, String(f[0]), x0, x1, "moss", x0 > 0.0, x1 < float(EAST_LIMIT), FLOOR_Y, GROUND_TONE) + floor_strip(host, String(f[0]), x0, x1, "stone", x0 > 0.0, x1 < float(EAST_LIMIT), FLOOR_Y, GROUND_TONE) for p in PITS: _slag_pit(host, String(p[0]), p[1], p[2]) for s in STEPS: - step(host, String(s[0]), s[1], s[2], "moss_float", GROUND_TONE) - platform(host, "SlagPillar", PILLAR_POS, PILLAR_SIZE, "moss", true, true, GROUND_TONE) + step(host, String(s[0]), s[1], s[2], "floating", GROUND_TONE) + platform(host, "SlagPillar", PILLAR_POS, PILLAR_SIZE, "stone", true, true, GROUND_TONE) for g in GEARS: var octagon := gear(host, String(g[0]), g[1], g[2]) octagon.modulate = LIFT_TONE - enclose(host, "moss", CEILING_Y, -1.0, 0.0, GROUND_TONE) + enclose(host, "stone", CEILING_Y, -1.0, 0.0, GROUND_TONE) var lift_a := lift(host, "LiftA", LIFT_A_POS, LIFT_A_TRAVEL, LIFT_PERIOD, LIFT_WIDTH, 0.0) lift_a.modulate = LIFT_TONE var lift_b := lift(host, "LiftB", LIFT_B_POS, LIFT_B_TRAVEL, LIFT_PERIOD, LIFT_WIDTH, LIFT_B_PHASE) @@ -185,7 +185,7 @@ func _build_terrain(host: Node2D) -> void: func _slag_pit(host: Node2D, pit_name: String, x0: float, x1: float) -> ToxinPool: var bed_y := FLOOR_Y + PIT_DEPTH platform(host, pit_name + "Bed", Vector2(x0, bed_y), Vector2(x1 - x0, FLOOR_Y + 80.0 - bed_y), - "moss", false, false, GROUND_TONE) + "stone", false, false, GROUND_TONE) return toxin(host, pit_name + "Pool", Vector2(x0, bed_y - 16.0), Vector2(x1 - x0, 32.0), SLAG_TINT) @@ -210,11 +210,17 @@ func _build_enemies(host: Node2D) -> void: func _build_decor(host: Node2D) -> void: for x in TORCHES: torch(host, x, FLOOR_Y, TORCH_TINT) - for x in VINES: + # 旧缆减半,染成炉渣锈色;地面用干石/石板,支柱用工字梁。 + var cable_xs: Array = [] + for i in range(VINES.size()): + if i % 2 == 0: + cable_xs.append(VINES[i]) + for x in cable_xs: hanging(host, "vine", Vector2(x, CEILING_Y + 32.0), VINE_TINT) for x in ARCHES: - prop(host, "arch", Vector2(x, FLOOR_Y), 1.0, ARCH_TINT) - for x in RUBBLE: - prop(host, "rubble", Vector2(x, FLOOR_Y), 1.0, RUBBLE_TINT) + prop(host, "slab", Vector2(x, FLOOR_Y), 1.15, ARCH_TINT) + var rubble_keys := ["rubble_dry", "rubble_dry_b", "rubble_dry_c"] + for i in range(RUBBLE.size()): + prop(host, rubble_keys[i % rubble_keys.size()], Vector2(RUBBLE[i], FLOOR_Y), 1.0, RUBBLE_TINT) for x in COLUMNS: - prop(host, "column", Vector2(x, FLOOR_Y), 1.0, COLUMN_TINT) + prop(host, "beam_bolts", Vector2(x, FLOOR_Y), 1.2, COLUMN_TINT) From 249b3dd7a6d7d98e8a53f34c627198a9d5506b10 Mon Sep 17 00:00:00 2001 From: Chen Runsen <139300395+SSC-STUDIO@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:56:49 +0800 Subject: [PATCH 5/6] fix(levels): replace moss/cathedral props on industrial and forge maps Use Kenney beams/metal and dry rubble on level05/08/10, and give forge its own industrial parallax instead of castle interior stained glass. (scripts/levels/chapters/level05_layout.gd) --- scripts/levels/chapters/level05_layout.gd | 38 +++++++++++------------ 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/scripts/levels/chapters/level05_layout.gd b/scripts/levels/chapters/level05_layout.gd index 43071e9..795359f 100644 --- a/scripts/levels/chapters/level05_layout.gd +++ b/scripts/levels/chapters/level05_layout.gd @@ -222,27 +222,27 @@ func _build_enemies(host: Node2D) -> void: ## -------------------------------------------------------------------- decor -- func _build_decor(host: Node2D) -> void: - # A 入口车间:来路的门洞、壁炬、碎石。 - prop(host, "door_arch", Vector2(96, FLOOR_Y)) + # A 入口车间:金属梁、壁炬、干石渣(不再用苔门拱/教堂柱)。 + prop(host, "beam_bolts", Vector2(96, FLOOR_Y), 1.0, Color(0.75, 0.7, 0.65)) torch(host, 440.0, FLOOR_Y, TORCH_TINT) - prop(host, "rubble", Vector2(456, FLOOR_Y)) - # B 淬火槽:坑沿石栏,走道下的壁炬(火焰在走道底下 16px),台阶靠着一根大柱。 - prop(host, "balustrade", Vector2(728, FLOOR_Y)) + prop(host, "rubble_dry", Vector2(456, FLOOR_Y)) + # B 淬火槽:坑沿钢板,走道栏改短梁;支柱用带螺栓的工字梁。 + prop(host, "plate", Vector2(728, FLOOR_Y), 1.0, Color(0.7, 0.62, 0.55)) torch(host, 760.0, FLOOR_Y, TORCH_TINT) - prop(host, "balustrade", Vector2(840, WALK_Y)) - prop(host, "rubble", Vector2(900, FLOOR_Y)) - prop(host, "column", Vector2(990, FLOOR_Y)) - # D 压板塔:巢边壁炬、盲门洞、碎石;压板台上一段石栏;走廊尽头一根大柱。 + prop(host, "beam_narrow", Vector2(840, WALK_Y), 1.0, Color(0.72, 0.66, 0.58)) + prop(host, "rubble_dry_b", Vector2(900, FLOOR_Y)) + prop(host, "beam", Vector2(990, FLOOR_Y), 1.15, Color(0.68, 0.58, 0.5)) + # D 压板塔:巢边壁炬、金属门板、干渣;走廊尽头工字梁。 torch(host, 1688.0, FLOOR_Y, TORCH_TINT) - prop(host, "balustrade", Vector2(1736, DECK_Y)) - prop(host, "door_arch", Vector2(1760, FLOOR_Y)) - prop(host, "rubble", Vector2(1808, FLOOR_Y)) - prop(host, "column", Vector2(1990, FLOOR_Y)) - # F 锈门与运渣道:落点碎石、台阶靠柱、走道西端石栏、锈门前壁炬。 - prop(host, "rubble", Vector2(2536, FLOOR_Y)) - prop(host, "column", Vector2(2672, FLOOR_Y)) - prop(host, "balustrade", Vector2(2856, WALK_Y)) + prop(host, "beam_narrow", Vector2(1736, DECK_Y), 1.0, Color(0.72, 0.66, 0.58)) + prop(host, "metal_panel", Vector2(1760, FLOOR_Y), 1.2, Color(0.7, 0.6, 0.52)) + prop(host, "rubble_dry_c", Vector2(1808, FLOOR_Y)) + prop(host, "beam_bolts", Vector2(1990, FLOOR_Y), 1.1, Color(0.68, 0.58, 0.5)) + # F 锈门与运渣道。 + prop(host, "rubble_dry", Vector2(2536, FLOOR_Y)) + prop(host, "beam", Vector2(2672, FLOOR_Y), 1.15, Color(0.68, 0.58, 0.5)) + prop(host, "plate", Vector2(2856, WALK_Y), 1.0, Color(0.7, 0.62, 0.55)) torch(host, 2832.0, FLOOR_Y, TORCH_TINT) - # 顶板垂下的旧缆:少量,避开吊台链子扫过的范围。 - for x in [300.0, 600.0, 1560.0, 2090.0, 2596.0]: + # 顶板垂下的旧缆:仍用窄藤条资源,但染成锈缆色,数量更少。 + for x in [300.0, 1560.0, 2596.0]: hanging(host, "vine", Vector2(x, 0), CABLE_TINT) From 87b023d49efa06752602eae9219921495775403c Mon Sep 17 00:00:00 2001 From: Chen Runsen <139300395+SSC-STUDIO@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:57:37 +0800 Subject: [PATCH 6/6] fix(levels): replace moss/cathedral props on industrial and forge maps Use Kenney beams/metal and dry rubble on level05/08/10, and give forge its own industrial parallax instead of castle interior stained glass. Add industrial DECOR keys (beam, metal, dry rubble). --- scripts/levels/chapter_layout.gd | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scripts/levels/chapter_layout.gd b/scripts/levels/chapter_layout.gd index 317ae22..1e62aff 100644 --- a/scripts/levels/chapter_layout.gd +++ b/scripts/levels/chapter_layout.gd @@ -53,6 +53,16 @@ const DECOR := { "altar": "res://assets/env/bg_altar.png", # 祭坛墙板(背景,接地) "window": "res://assets/env/bg_wall_window.png", # 尖窗墙板(背景,接地) "skull_column": "res://assets/env/bg_column_skulls.png", # 骷髅柱(背景,接地) + # 工业/熔炉:Kenney Request pack + 干石渣(避免苔拱/教堂柱串味) + "beam": "res://assets/external/kenney/Art (5190 files)/Platformer assets (1330 assets)/Request pack (100 assets)/PNG/beam.png", + "beam_bolts": "res://assets/external/kenney/Art (5190 files)/Platformer assets (1330 assets)/Request pack (100 assets)/PNG/beamBolts.png", + "beam_narrow": "res://assets/external/kenney/Art (5190 files)/Platformer assets (1330 assets)/Request pack (100 assets)/PNG/beamNarrow.png", + "metal_panel": "res://assets/external/kenney/Art (5190 files)/Platformer assets (1330 assets)/Request pack (100 assets)/PNG/metal.png", + "rubble_dry": "res://assets/env/rubble_a.png", + "rubble_dry_b": "res://assets/env/rubble_b.png", + "rubble_dry_c": "res://assets/env/rubble_c.png", + "slab": "res://assets/env/slab_a.png", + "plate": "res://assets/env/plate_stone.png", "statue": "res://assets/env/statue_keeper.png", # 守墓者石像,接地 "grave_cross": "res://assets/env/grave_cross.png", # 十字墓碑,接地 "door_arch": "res://assets/env/door_arch.png", # 32×64 石门洞(装饰),接地