From 773b15fd8ef8c72e46be06db5def5f973b0b6d2b Mon Sep 17 00:00:00 2001 From: Dmitrii Bykov Date: Sun, 24 May 2026 18:04:50 +0300 Subject: [PATCH] =?UTF-8?q?act1:=20=D1=84=D0=B8=D0=BA=D1=81=D1=8B=20=D0=B1?= =?UTF-8?q?=D0=B0=D0=B3=D0=BE=D0=B2=20+=20crosshair=20+=20=D1=83=D0=BB?= =?UTF-8?q?=D1=83=D1=87=D1=88=D0=B5=D0=BD=D0=BD=D1=8B=D0=B5=20=D1=82=D0=B5?= =?UTF-8?q?=D0=BA=D1=81=D1=82=D1=83=D1=80=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit БАГ-ФИКСЫ: - Трясёт у двери: Interactable теперь extends Area3D (не StaticBody3D). RayCast3D детектит Area3D через collide_with_areas=true. Игрок физически проходит через интерактивы (gift box, флешка, записка, дверь) — не толкается в узких местах. Бампается только в стены/мебель/пол. - Все 6 интерактивов в act1_room.tscn и act1_kitchen.tscn переведены на type="Area3D". UI: - Crosshair в центре экрана: 8×8 точка с тёмной рамкой и белой центральной 2×2 точкой. set_crosshair_visible() для 2D-сцен (Акт 2 прячет его). - SubtitlePanel расширен: 16-624 × 240-354 (было 24-616 × 270-350). Text-лейбл заменён на RichTextLabel с fit_content + bbcode. Длинные реплики отца теперь нормально влезают. - NoteOverlay аналогично RichTextLabel. - ControlsHint перенесён сверху (под HUD-датой), 14px меньше. ТЕКСТУРЫ через FastNoiseLite + 128×128 разрешалка: - wallpaper: шумная бежевая база + растительный крест-цветок каждые 22px, шахматное смещение, тонкая горизонтальная полоса каждые 32px (стык обоев). - linoleum: настоящая паркетная доска 16×64px со смещением между рядами, тонкие швы, волокно через шум. - ceiling: побелка с пятном протёчки в углу (canon-деталь). - wall_carpet: бордовая база + двойная золотая рамка + ромб- медальон с золотыми углами + декоративные уголки. - blanket: 30 звёздочек (белые/жёлтые) разного размера на шумной тёмно-синей базе. - poster_csgo: диагональная штриховка + силуэт AWP по центру + пиксельный «CS:GO» текст внизу. - poster_morgen: круг с лицом, глаза-щели, рот + пиксельный «MS». - door: вертикальное волокно Perlin + 3 горизонтальных шва (панели) + 4 сучка разного размера. - desk_white: чистый белый + жёлтый Steam-стикер в углу + царапина. Co-Authored-By: Claude Opus 4.7 (1M context) --- scenes/components/dialog_ui.tscn | 73 +++- scenes/components/player_fpv.tscn | 1 + scenes/prologue/act1_kitchen.tscn | 4 +- scenes/prologue/act1_room.tscn | 8 +- scripts/interactables/interactable.gd | 3 +- scripts/prologue/act2_install.gd | 1 + scripts/ui/dialog_manager.gd | 11 +- scripts/util/texture_generator.gd | 580 +++++++++++++++++++------- 8 files changed, 498 insertions(+), 183 deletions(-) diff --git a/scenes/components/dialog_ui.tscn b/scenes/components/dialog_ui.tscn index c66a77a..b741147 100644 --- a/scenes/components/dialog_ui.tscn +++ b/scenes/components/dialog_ui.tscn @@ -34,44 +34,73 @@ modulate = Color(0.75, 0.75, 0.75, 1) [node name="WeatherLabel" type="Label" parent="."] offset_left = 340.0 -offset_top = 332.0 +offset_top = 215.0 offset_right = 632.0 -offset_bottom = 348.0 +offset_bottom = 231.0 text = "" horizontal_alignment = 2 modulate = Color(0.7, 0.75, 0.85, 1) [node name="StatsLabel" type="Label" parent="."] offset_left = 8.0 -offset_top = 332.0 +offset_top = 215.0 offset_right = 300.0 -offset_bottom = 348.0 +offset_bottom = 231.0 text = "" modulate = Color(0.94, 0.71, 0.1, 1) [node name="ControlsHint" type="Label" parent="."] offset_left = 0.0 -offset_top = 246.0 +offset_top = 28.0 offset_right = 640.0 -offset_bottom = 260.0 -text = "WASD — ходить · мышь — смотреть · ЛКМ — взаимодействие · ESC — мышь · Q — выход" +offset_bottom = 42.0 +text = "WASD ходить · мышь смотреть · ЛКМ взаимодействие · ESC мышь · Q выход" horizontal_alignment = 1 -modulate = Color(0.5, 0.5, 0.5, 0.8) +modulate = Color(0.45, 0.45, 0.45, 0.8) + +[node name="Crosshair" type="Control" parent="."] +offset_left = 316.0 +offset_top = 176.0 +offset_right = 324.0 +offset_bottom = 184.0 + +[node name="CenterDot" type="ColorRect" parent="Crosshair"] +offset_left = 3.0 +offset_top = 3.0 +offset_right = 5.0 +offset_bottom = 5.0 +color = Color(1, 1, 1, 0.85) + +[node name="OuterRing" type="ColorRect" parent="Crosshair"] +offset_left = 0.0 +offset_top = 0.0 +offset_right = 8.0 +offset_bottom = 8.0 +color = Color(0, 0, 0, 0.4) +mouse_filter = 2 + +[node name="InnerHole" type="ColorRect" parent="Crosshair"] +offset_left = 1.0 +offset_top = 1.0 +offset_right = 7.0 +offset_bottom = 7.0 +color = Color(0, 0, 0, 0) +mouse_filter = 2 [node name="PromptLabel" type="Label" parent="."] offset_left = 0.0 -offset_top = 200.0 +offset_top = 192.0 offset_right = 640.0 -offset_bottom = 220.0 +offset_bottom = 212.0 text = "" horizontal_alignment = 1 modulate = Color(0.94, 0.71, 0.1, 1) [node name="SubtitlePanel" type="PanelContainer" parent="."] -offset_left = 24.0 -offset_top = 270.0 -offset_right = 616.0 -offset_bottom = 350.0 +offset_left = 16.0 +offset_top = 240.0 +offset_right = 624.0 +offset_bottom = 354.0 [node name="V" type="VBoxContainer" parent="SubtitlePanel"] layout_mode = 2 @@ -81,9 +110,13 @@ layout_mode = 2 text = "[Я]" modulate = Color(0.94, 0.71, 0.1, 1) -[node name="Text" type="Label" parent="SubtitlePanel/V"] +[node name="Text" type="RichTextLabel" parent="SubtitlePanel/V"] +custom_minimum_size = Vector2(590, 60) layout_mode = 2 +bbcode_enabled = true text = "" +fit_content = true +scroll_active = false autowrap_mode = 3 [node name="Hint" type="Label" parent="SubtitlePanel/V"] @@ -104,7 +137,7 @@ anchor_right = 1.0 anchor_bottom = 1.0 offset_right = 640.0 offset_bottom = 360.0 -color = Color(0, 0, 0, 0.75) +color = Color(0, 0, 0, 0.78) [node name="Center" type="CenterContainer" parent="NoteOverlay"] anchor_right = 1.0 @@ -113,7 +146,7 @@ offset_right = 640.0 offset_bottom = 360.0 [node name="P" type="PanelContainer" parent="NoteOverlay/Center"] -custom_minimum_size = Vector2(420, 180) +custom_minimum_size = Vector2(440, 200) layout_mode = 2 [node name="V2" type="VBoxContainer" parent="NoteOverlay/Center/P"] @@ -125,11 +158,13 @@ text = "Записка" horizontal_alignment = 1 modulate = Color(0.94, 0.71, 0.1, 1) -[node name="Text" type="Label" parent="NoteOverlay/Center/P/V2"] +[node name="Text" type="RichTextLabel" parent="NoteOverlay/Center/P/V2"] +custom_minimum_size = Vector2(420, 100) layout_mode = 2 +bbcode_enabled = true text = "" +fit_content = true autowrap_mode = 3 -horizontal_alignment = 1 [node name="Hint" type="Label" parent="NoteOverlay/Center/P/V2"] layout_mode = 2 diff --git a/scenes/components/player_fpv.tscn b/scenes/components/player_fpv.tscn index ba9df0f..1fe11b7 100644 --- a/scenes/components/player_fpv.tscn +++ b/scenes/components/player_fpv.tscn @@ -23,3 +23,4 @@ far = 50.0 [node name="InteractRay" type="RayCast3D" parent="Camera3D"] target_position = Vector3(0, 0, -2.5) collision_mask = 1 +collide_with_areas = true diff --git a/scenes/prologue/act1_kitchen.tscn b/scenes/prologue/act1_kitchen.tscn index f9982eb..db7a987 100644 --- a/scenes/prologue/act1_kitchen.tscn +++ b/scenes/prologue/act1_kitchen.tscn @@ -212,7 +212,7 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.7, -0.25, -0.06) mesh = SubResource("mesh_door_panel") material_override = SubResource("mat_door") -[node name="DoorBack" type="StaticBody3D" parent="WallS"] +[node name="DoorBack" type="Area3D" parent="WallS"] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.7, -0.25, -0.12) script = ExtResource("2_int") prompt = "вернуться в комнату" @@ -254,7 +254,7 @@ material_override = SubResource("mat_fridge") [node name="Coll" type="CollisionShape3D" parent="Fridge"] shape = SubResource("shape_fridge") -[node name="WiFiNote" type="StaticBody3D" parent="Fridge"] +[node name="WiFiNote" type="Area3D" parent="Fridge"] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.6, 0.31) script = ExtResource("2_int") prompt = "прочитать записку" diff --git a/scenes/prologue/act1_room.tscn b/scenes/prologue/act1_room.tscn index 234a0fe..81ffc76 100644 --- a/scenes/prologue/act1_room.tscn +++ b/scenes/prologue/act1_room.tscn @@ -396,7 +396,7 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.06, -0.25, 0) mesh = SubResource("mesh_door") material_override = SubResource("mat_door") -[node name="Door" type="StaticBody3D" parent="WallE"] +[node name="Door" type="Area3D" parent="WallE"] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.1, -0.25, 0) script = ExtResource("2_int") prompt = "выйти на кухню" @@ -493,7 +493,7 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.1, 0.36, 0.0) mesh = SubResource("mesh_lays") material_override = SubResource("mat_lays") -[node name="FlashDrive" type="StaticBody3D" parent="Desk"] +[node name="FlashDrive" type="Area3D" parent="Desk"] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.4, 0.36, 0.15) script = ExtResource("2_int") prompt = "взять флешку" @@ -535,7 +535,7 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.85, 0.015, 0.2) mesh = SubResource("mesh_sock") material_override = SubResource("mat_sock") -[node name="GiftBox" type="StaticBody3D" parent="."] +[node name="GiftBox" type="Area3D" parent="."] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.2, -0.3) script = ExtResource("2_int") prompt = "открыть коробку" @@ -555,7 +555,7 @@ visible = false mesh = SubResource("mesh_sys") material_override = SubResource("mat_sys") -[node name="Note" type="StaticBody3D" parent="SystemUnit"] +[node name="Note" type="Area3D" parent="SystemUnit"] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.26, 0) script = ExtResource("2_int") prompt = "прочитать записку" diff --git a/scripts/interactables/interactable.gd b/scripts/interactables/interactable.gd index fcfac73..3fc7237 100644 --- a/scripts/interactables/interactable.gd +++ b/scripts/interactables/interactable.gd @@ -1,4 +1,4 @@ -class_name Interactable extends StaticBody3D +class_name Interactable extends Area3D @export var prompt: String = "Взаимодействовать" @export var enabled: bool = false @@ -7,6 +7,7 @@ signal interacted(player: Node) func _ready() -> void: + monitoring = false _apply_collision() diff --git a/scripts/prologue/act2_install.gd b/scripts/prologue/act2_install.gd index 6a54526..638bd7b 100644 --- a/scripts/prologue/act2_install.gd +++ b/scripts/prologue/act2_install.gd @@ -49,6 +49,7 @@ func _ready() -> void: Input.mouse_mode = Input.MOUSE_MODE_VISIBLE DialogManager.clear_prompt() DialogManager.set_controls_hint("") + DialogManager.set_crosshair_visible(false) DialogManager.set_date_time(GameState.game_date, GameState.game_time) DialogManager.set_objective("Установи Шиндовс 9") diff --git a/scripts/ui/dialog_manager.gd b/scripts/ui/dialog_manager.gd index bed49eb..b6122ad 100644 --- a/scripts/ui/dialog_manager.gd +++ b/scripts/ui/dialog_manager.gd @@ -4,14 +4,15 @@ signal advanced @onready var subtitle_panel: PanelContainer = $SubtitlePanel @onready var who_label: Label = $SubtitlePanel/V/Who -@onready var text_label: Label = $SubtitlePanel/V/Text +@onready var text_label: RichTextLabel = $SubtitlePanel/V/Text @onready var hint_label: Label = $SubtitlePanel/V/Hint @onready var note_overlay: Control = $NoteOverlay -@onready var note_text: Label = $NoteOverlay/Center/P/V2/Text +@onready var note_text: RichTextLabel = $NoteOverlay/Center/P/V2/Text @onready var prompt_label: Label = $PromptLabel @onready var controls_hint: Label = $ControlsHint +@onready var crosshair: Control = $Crosshair @onready var date_time_label: Label = $DateTimeLabel @onready var objective_label: Label = $ObjectiveLabel @@ -30,7 +31,11 @@ func _ready() -> void: money_label.visible = false weather_label.visible = false stats_label.visible = false - # ControlsHint всегда виден, не блокирует игру + crosshair.visible = true + + +func set_crosshair_visible(v: bool) -> void: + crosshair.visible = v func say(who: String, line: String) -> Signal: diff --git a/scripts/util/texture_generator.gd b/scripts/util/texture_generator.gd index 58cd8cd..f04afa7 100644 --- a/scripts/util/texture_generator.gd +++ b/scripts/util/texture_generator.gd @@ -21,98 +21,211 @@ func _get_or_create(key: String, maker: Callable) -> ImageTexture: return tex -# Бежевые обои с растительным узором (мелкие точки) +func _noise(seed_v: int, freq: float, ntype: int = FastNoiseLite.TYPE_PERLIN) -> FastNoiseLite: + var n: FastNoiseLite = FastNoiseLite.new() + n.seed = seed_v + n.frequency = freq + n.noise_type = ntype + return n + + +# Бежевые обои — растительный узор поверх шумной базы func wallpaper() -> ImageTexture: return _get_or_create("wallpaper", func() -> ImageTexture: + var size: int = 128 var base: Color = Color(0.83, 0.72, 0.55) - var dot: Color = Color(0.72, 0.6, 0.42) - var darker: Color = Color(0.78, 0.68, 0.5) - var img: Image = _make_image(64, 64, base) - # мелкие крестики-цветы - for y in range(0, 64, 8): - for x in range(0, 64, 8): - var ox: int = 4 if (y / 8) % 2 == 0 else 0 - var px: int = (x + ox) % 64 - img.set_pixel(px, y, dot) - if px + 1 < 64: - img.set_pixel(px + 1, y, darker) - if y + 1 < 64: - img.set_pixel(px, y + 1, darker) + var darker: Color = Color(0.7, 0.58, 0.42) + var darker_dark: Color = Color(0.58, 0.46, 0.32) + var img: Image = _make_image(size, size, base) + + var noise: FastNoiseLite = _noise(424242, 0.06) + + # Шумная база + for y in range(size): + for x in range(size): + var n: float = noise.get_noise_2d(x, y) * 0.5 + 0.5 + var c: Color = base.lerp(darker, n * 0.18) + # Лёгкая горизонтальная полоса (стык обоев каждые 32px) + if y % 32 == 0: + c = c.darkened(0.04) + img.set_pixel(x, y, c) + + # Цветочки шахматно + var spacing: int = 22 + for row in range(0, size / spacing + 1): + for col in range(0, size / spacing + 1): + var offset_x: int = (spacing / 2) if row % 2 == 0 else 0 + var cx: int = (col * spacing + offset_x) % size + var cy: int = row * spacing + if cy >= size: + continue + # Лепестки крестом 3х3, центр темнее + _draw_flower(img, cx, cy, size, darker, darker_dark) + return _to_texture(img) ) -# Линолеум жёлто-коричневый с горизонтальными полосами под паркет +func _draw_flower(img: Image, cx: int, cy: int, size: int, petal: Color, center: Color) -> void: + var w: int = img.get_width() + var h: int = img.get_height() + # 5-точечный крест + var pts: Array = [ + [0, 0, center], + [-1, 0, petal], [1, 0, petal], + [0, -1, petal], [0, 1, petal], + [-2, 0, petal], [2, 0, petal], + [0, -2, petal], [0, 2, petal], + ] + for p in pts: + var x: int = cx + p[0] + var y: int = cy + p[1] + if x >= 0 and x < w and y >= 0 and y < h: + img.set_pixel(x, y, p[2]) + + +# Линолеум: «под светлый паркет» — доски с волокном func linoleum() -> ImageTexture: return _get_or_create("linoleum", func() -> ImageTexture: - var a: Color = Color(0.72, 0.6, 0.41) - var b: Color = Color(0.65, 0.53, 0.36) - var c: Color = Color(0.58, 0.46, 0.3) - var img: Image = _make_image(64, 64, a) - # полосы паркета - for y in range(64): - var stripe: int = y / 8 - var col: Color = a if stripe % 2 == 0 else b - for x in range(64): - img.set_pixel(x, y, col) - # швы между досками - for y in range(0, 64, 8): - for x in range(64): - img.set_pixel(x, y, c) - # вертикальные швы пэчворк - for y in range(64): - var stripe: int = y / 8 - var seam_x: int = ((stripe * 23) % 32) + 16 - img.set_pixel(seam_x, y, c) + var size: int = 128 + var base: Color = Color(0.72, 0.6, 0.41) + var dark: Color = Color(0.55, 0.43, 0.28) + var darker: Color = Color(0.4, 0.3, 0.18) + var lighter: Color = Color(0.85, 0.74, 0.55) + var img: Image = _make_image(size, size, base) + + var grain_noise: FastNoiseLite = _noise(1234, 0.25) + var plank_h: int = 16 # высота доски + var plank_w: int = 64 # ширина + + for y in range(size): + var plank_row: int = y / plank_h + var plank_offset: int = (plank_row * 23) % plank_w + for x in range(size): + # Тон доски варьируется + var plank_idx: int = (x + plank_offset) / plank_w + var plank_tint: float = float((plank_row + plank_idx) % 4) / 4.0 + var tinted: Color = base.lerp(lighter if plank_tint > 0.5 else dark, plank_tint * 0.25) + # Волокно — вертикальные тонкие штрихи через шум + var grain: float = grain_noise.get_noise_2d(x, y * 4) * 0.5 + 0.5 + tinted = tinted.lerp(dark, grain * 0.25) + img.set_pixel(x, y, tinted) + + # Швы между досками (горизонтальные) + for y in range(0, size, plank_h): + for x in range(size): + img.set_pixel(x, y, darker) + # Швы между досками (вертикальные, смещённые) + for row in range(size / plank_h): + var offset: int = (row * 23) % plank_w + var y_start: int = row * plank_h + for col in range(size / plank_w + 1): + var seam_x: int = (col * plank_w + offset) % size + for dy in range(1, plank_h): + if y_start + dy < size: + img.set_pixel(seam_x, y_start + dy, darker) + return _to_texture(img) ) -# Потолок: светлая побелка с лёгким шумом +# Потолок: побелка с очень лёгким шумом + пятно протёчки func ceiling() -> ImageTexture: return _get_or_create("ceiling", func() -> ImageTexture: + var size: int = 64 var base: Color = Color(0.92, 0.9, 0.85) - var spot: Color = Color(0.88, 0.86, 0.82) - var img: Image = _make_image(32, 32, base) - var rng: RandomNumberGenerator = RandomNumberGenerator.new() - rng.seed = 4242 - for i in range(20): - var x: int = rng.randi_range(0, 31) - var y: int = rng.randi_range(0, 31) - img.set_pixel(x, y, spot) + var spot: Color = Color(0.85, 0.82, 0.74) + var stain: Color = Color(0.78, 0.72, 0.62) + var img: Image = _make_image(size, size, base) + var noise: FastNoiseLite = _noise(8888, 0.12) + for y in range(size): + for x in range(size): + var n: float = noise.get_noise_2d(x, y) * 0.5 + 0.5 + var c: Color = base.lerp(spot, n * 0.18) + img.set_pixel(x, y, c) + # Пятно от протёчки в углу + var cx: int = 12 + var cy: int = 12 + for r in range(8): + for ang in range(8): + var a: float = float(ang) * PI / 4.0 + var px: int = cx + int(cos(a) * r) + var py: int = cy + int(sin(a) * r) + if px >= 0 and px < size and py >= 0 and py < size: + var t: float = 1.0 - (float(r) / 8.0) + img.set_pixel(px, py, base.lerp(stain, t * 0.4)) return _to_texture(img) ) -# Бордовый ковёр с персидским ромбовидным узором +# Бордовый ковёр персидский с медальоном func wall_carpet() -> ImageTexture: return _get_or_create("wall_carpet", func() -> ImageTexture: + var size: int = 128 var base: Color = Color(0.36, 0.12, 0.13) - var pattern: Color = Color(0.78, 0.62, 0.2) var dark: Color = Color(0.22, 0.07, 0.08) - var img: Image = _make_image(64, 64, base) - # ромбы - for cy in [16, 48]: - for cx in [16, 48]: - for r in range(6): - var px1: int = cx - r - var px2: int = cx + r - var py1: int = cy - (6 - r) - var py2: int = cy + (6 - r) - if px1 >= 0 and py1 >= 0 and px1 < 64 and py1 < 64: - img.set_pixel(px1, py1, pattern) - if px2 < 64 and py1 >= 0: - img.set_pixel(px2, py1, pattern) - if px1 >= 0 and py2 < 64: - img.set_pixel(px1, py2, pattern) - if px2 < 64 and py2 < 64: - img.set_pixel(px2, py2, pattern) - # рамка - for i in range(64): - img.set_pixel(i, 0, dark) - img.set_pixel(i, 63, dark) - img.set_pixel(0, i, dark) - img.set_pixel(63, i, dark) + var gold: Color = Color(0.78, 0.62, 0.2) + var cream: Color = Color(0.85, 0.75, 0.5) + var img: Image = _make_image(size, size, base) + + var noise: FastNoiseLite = _noise(5555, 0.1) + # Шумная основа + for y in range(size): + for x in range(size): + var n: float = noise.get_noise_2d(x, y) * 0.5 + 0.5 + img.set_pixel(x, y, base.lerp(dark, n * 0.25)) + + # Внешняя рамка + for i in range(size): + for t in range(3): + img.set_pixel(i, t, dark) + img.set_pixel(i, size - 1 - t, dark) + img.set_pixel(t, i, dark) + img.set_pixel(size - 1 - t, i, dark) + # Внутренняя золотая полоса + for i in range(6, size - 6): + img.set_pixel(i, 5, gold) + img.set_pixel(i, size - 6, gold) + img.set_pixel(5, i, gold) + img.set_pixel(size - 6, i, gold) + + # Центральный медальон-ромб + var cx: int = size / 2 + var cy: int = size / 2 + var med_r: int = 32 + for r in range(med_r): + var w: int = med_r - r + for dx in range(-w, w + 1): + var py1: int = cy - r + var py2: int = cy + r + var px: int = cx + dx + if px >= 0 and px < size and py1 >= 0: + if r == med_r - 1 or abs(dx) == w: + img.set_pixel(px, py1, gold) + elif r % 4 == 0 and abs(dx) % 4 == 0: + img.set_pixel(px, py1, cream) + if px >= 0 and px < size and py2 < size: + if r == med_r - 1 or abs(dx) == w: + img.set_pixel(px, py2, gold) + elif r % 4 == 0 and abs(dx) % 4 == 0: + img.set_pixel(px, py2, cream) + + # Уголки + var corners: Array = [[20, 20], [size - 20, 20], [20, size - 20], [size - 20, size - 20]] + for c in corners: + for r in range(8): + var w: int = 8 - r + for dx in range(-w, w + 1): + var py: int = c[1] + r + var px: int = c[0] + dx + if px >= 0 and px < size and py >= 0 and py < size: + if abs(dx) == w or r == 7: + img.set_pixel(px, py, gold) + py = c[1] - r + if px >= 0 and px < size and py >= 0 and py < size: + if abs(dx) == w or r == 7: + img.set_pixel(px, py, gold) + return _to_texture(img) ) @@ -120,123 +233,273 @@ func wall_carpet() -> ImageTexture: # Тёмно-синее одеяло «звёзды» func blanket() -> ImageTexture: return _get_or_create("blanket", func() -> ImageTexture: + var size: int = 64 var base: Color = Color(0.13, 0.16, 0.32) - var star: Color = Color(0.85, 0.85, 0.75) - var img: Image = _make_image(32, 32, base) - var stars: Array = [[5,5],[14,3],[24,7],[8,12],[20,16],[3,21],[27,22],[15,26],[10,28]] - for s in stars: - img.set_pixel(s[0], s[1], star) - if s[0] + 1 < 32: - img.set_pixel(s[0] + 1, s[1], star) - if s[0] - 1 >= 0: - img.set_pixel(s[0] - 1, s[1], star) - if s[1] + 1 < 32: - img.set_pixel(s[0], s[1] + 1, star) - if s[1] - 1 >= 0: - img.set_pixel(s[0], s[1] - 1, star) + var deeper: Color = Color(0.09, 0.11, 0.24) + var star_w: Color = Color(0.9, 0.88, 0.78) + var star_y: Color = Color(0.94, 0.85, 0.45) + var img: Image = _make_image(size, size, base) + + var noise: FastNoiseLite = _noise(7777, 0.08) + for y in range(size): + for x in range(size): + var n: float = noise.get_noise_2d(x, y) * 0.5 + 0.5 + img.set_pixel(x, y, base.lerp(deeper, n * 0.4)) + + var rng: RandomNumberGenerator = RandomNumberGenerator.new() + rng.seed = 9999 + for i in range(30): + var sx: int = rng.randi_range(2, size - 3) + var sy: int = rng.randi_range(2, size - 3) + var c: Color = star_y if rng.randf() > 0.7 else star_w + var big: bool = rng.randf() > 0.7 + img.set_pixel(sx, sy, c) + if big: + img.set_pixel(sx + 1, sy, c) + img.set_pixel(sx - 1, sy, c) + img.set_pixel(sx, sy + 1, c) + img.set_pixel(sx, sy - 1, c) return _to_texture(img) ) -# CS:GO постер (жёлтый с силуэтом AWP) +# Постер CS:GO — жёлтый с силуэтом AWP + надпись func poster_csgo() -> ImageTexture: return _get_or_create("poster_csgo", func() -> ImageTexture: + var w: int = 64 + var h: int = 80 var base: Color = Color(0.94, 0.71, 0.1) var dark: Color = Color(0.18, 0.13, 0.08) - var img: Image = _make_image(32, 40, base) - # рамка - for i in range(32): + var stripe: Color = Color(0.88, 0.65, 0.08) + var img: Image = _make_image(w, h, base) + + # Диагональная полоса + for y in range(h): + for x in range(w): + if (x + y) % 8 < 2: + img.set_pixel(x, y, stripe) + + # Рамка + for i in range(w): img.set_pixel(i, 0, dark) - img.set_pixel(i, 39, dark) - for i in range(40): + img.set_pixel(i, 1, dark) + img.set_pixel(i, h - 1, dark) + img.set_pixel(i, h - 2, dark) + for i in range(h): img.set_pixel(0, i, dark) - img.set_pixel(31, i, dark) - # буквы CS внизу (примитивно) - for x in range(8, 12): - img.set_pixel(x, 32, dark) - img.set_pixel(x, 35, dark) - img.set_pixel(8, 33, dark) - img.set_pixel(8, 34, dark) - for x in range(14, 18): - img.set_pixel(x, 32, dark) - img.set_pixel(x, 35, dark) - img.set_pixel(14, 33, dark) - img.set_pixel(17, 34, dark) - # силуэт AWP (горизонталь сверху) - for x in range(6, 26): - img.set_pixel(x, 18, dark) - img.set_pixel(x, 19, dark) - for x in range(20, 26): - img.set_pixel(x, 17, dark) - img.set_pixel(7, 20, dark) - img.set_pixel(8, 20, dark) + img.set_pixel(1, i, dark) + img.set_pixel(w - 1, i, dark) + img.set_pixel(w - 2, i, dark) + + # Силуэт AWP горизонтально по центру + var by: int = 30 + # приклад + for x in range(8, 14): + img.set_pixel(x, by + 2, dark) + img.set_pixel(x, by + 3, dark) + img.set_pixel(x, by + 4, dark) + # ствол + for x in range(14, 52): + img.set_pixel(x, by + 3, dark) + # прицел + for x in range(28, 36): + img.set_pixel(x, by + 1, dark) + for x in range(30, 34): + img.set_pixel(x, by, dark) + # рукоятка + img.set_pixel(18, by + 4, dark) + img.set_pixel(18, by + 5, dark) + img.set_pixel(17, by + 5, dark) + + # CS:GO текст внизу + _draw_text_csgo(img, 18, 60, dark) + return _to_texture(img) ) -# Моргенштерн постер (тёмно-красный с белым контуром головы) +func _draw_text_csgo(img: Image, x0: int, y0: int, c: Color) -> void: + # C + for dy in range(8): + img.set_pixel(x0, y0 + dy, c) + for dx in range(1, 5): + img.set_pixel(x0 + dx, y0, c) + img.set_pixel(x0 + dx, y0 + 7, c) + # S + var sx: int = x0 + 7 + for dx in range(5): + img.set_pixel(sx + dx, y0, c) + img.set_pixel(sx + dx, y0 + 3, c) + img.set_pixel(sx + dx, y0 + 7, c) + for dy in range(1, 3): + img.set_pixel(sx, y0 + dy, c) + for dy in range(4, 7): + img.set_pixel(sx + 4, y0 + dy, c) + # : + img.set_pixel(x0 + 14, y0 + 2, c) + img.set_pixel(x0 + 14, y0 + 5, c) + # G + var gx: int = x0 + 17 + for dy in range(8): + img.set_pixel(gx, y0 + dy, c) + for dx in range(1, 5): + img.set_pixel(gx + dx, y0, c) + img.set_pixel(gx + dx, y0 + 7, c) + img.set_pixel(gx + 4, y0 + 4, c) + img.set_pixel(gx + 4, y0 + 5, c) + img.set_pixel(gx + 4, y0 + 6, c) + img.set_pixel(gx + 3, y0 + 4, c) + # O + var ox: int = x0 + 24 + for dy in range(8): + img.set_pixel(ox, y0 + dy, c) + img.set_pixel(ox + 4, y0 + dy, c) + for dx in range(1, 4): + img.set_pixel(ox + dx, y0, c) + img.set_pixel(ox + dx, y0 + 7, c) + + +# Постер Моргенштерн — тёмно-красный с белой головой func poster_morgen() -> ImageTexture: return _get_or_create("poster_morgen", func() -> ImageTexture: + var w: int = 64 + var h: int = 80 var base: Color = Color(0.7, 0.13, 0.13) var light: Color = Color(0.9, 0.85, 0.8) var dark: Color = Color(0.2, 0.05, 0.05) - var img: Image = _make_image(32, 40, base) - # рамка - for i in range(32): + var pink: Color = Color(0.95, 0.7, 0.65) + var img: Image = _make_image(w, h, base) + + # Рамка + for i in range(w): img.set_pixel(i, 0, dark) - img.set_pixel(i, 39, dark) - for i in range(40): + img.set_pixel(i, 1, dark) + img.set_pixel(i, h - 1, dark) + img.set_pixel(i, h - 2, dark) + for i in range(h): img.set_pixel(0, i, dark) - img.set_pixel(31, i, dark) - # силуэт головы по центру - var cx: int = 16 - var cy: int = 18 - for r in range(7): - var w: int = int(sqrt(49 - r * r)) - for dx in range(-w, w + 1): - if cy - r >= 0 and cy - r < 40 and cx + dx >= 0 and cx + dx < 32: - img.set_pixel(cx + dx, cy - r, light) - if cy + r < 40 and cx + dx >= 0 and cx + dx < 32: - img.set_pixel(cx + dx, cy + r, light) + img.set_pixel(1, i, dark) + img.set_pixel(w - 1, i, dark) + img.set_pixel(w - 2, i, dark) + + # Голова в круге + var cx: int = w / 2 + var cy: int = 32 + var radius: int = 18 + for y in range(h): + for x in range(w): + var d: float = sqrt(pow(x - cx, 2) + pow(y - cy, 2)) + if d <= float(radius): + var t: float = d / float(radius) + img.set_pixel(x, y, light.lerp(pink, t * 0.6)) + if d > float(radius) - 1 and d <= float(radius): + img.set_pixel(x, y, dark) + + # Глаза-щели + img.set_pixel(cx - 5, cy - 2, dark) + img.set_pixel(cx - 4, cy - 2, dark) + img.set_pixel(cx + 4, cy - 2, dark) + img.set_pixel(cx + 5, cy - 2, dark) + # Рот + for dx in range(-4, 5): + img.set_pixel(cx + dx, cy + 6, dark) + + # Лого MS внизу + _draw_text_ms(img, 22, 60, light) return _to_texture(img) ) -# Двери: деревянная текстура с волокном +func _draw_text_ms(img: Image, x0: int, y0: int, c: Color) -> void: + # M + for dy in range(8): + img.set_pixel(x0, y0 + dy, c) + img.set_pixel(x0 + 6, y0 + dy, c) + img.set_pixel(x0 + 1, y0 + 1, c) + img.set_pixel(x0 + 5, y0 + 1, c) + img.set_pixel(x0 + 2, y0 + 2, c) + img.set_pixel(x0 + 4, y0 + 2, c) + img.set_pixel(x0 + 3, y0 + 3, c) + # S + var sx: int = x0 + 12 + for dx in range(5): + img.set_pixel(sx + dx, y0, c) + img.set_pixel(sx + dx, y0 + 3, c) + img.set_pixel(sx + dx, y0 + 7, c) + for dy in range(1, 3): + img.set_pixel(sx, y0 + dy, c) + for dy in range(4, 7): + img.set_pixel(sx + 4, y0 + dy, c) + + +# Дверь: вертикальные деревянные планки + сучки func door() -> ImageTexture: return _get_or_create("door", func() -> ImageTexture: - var a: Color = Color(0.36, 0.23, 0.12) - var b: Color = Color(0.45, 0.31, 0.18) - var c: Color = Color(0.28, 0.18, 0.09) - var img: Image = _make_image(32, 64, a) - for y in range(64): - for x in range(32): - var v: float = sin(float(x) * 0.5 + float(y) * 0.1) * 0.5 + 0.5 - if v > 0.6: - img.set_pixel(x, y, b) - elif v < 0.3: - img.set_pixel(x, y, c) + var w: int = 64 + var h: int = 128 + var base: Color = Color(0.45, 0.31, 0.18) + var dark: Color = Color(0.28, 0.18, 0.09) + var light: Color = Color(0.55, 0.4, 0.25) + var knot: Color = Color(0.18, 0.1, 0.05) + var img: Image = _make_image(w, h, base) + + # Вертикальное волокно через шум + var grain: FastNoiseLite = _noise(3333, 0.2) + grain.noise_type = FastNoiseLite.TYPE_PERLIN + for y in range(h): + for x in range(w): + var n: float = grain.get_noise_2d(x * 0.5, y * 3) * 0.5 + 0.5 + var c: Color = base.lerp(dark if n > 0.5 else light, abs(n - 0.5) * 1.2) + img.set_pixel(x, y, c) + + # Панели — горизонтальные швы + for y in [32, 64, 96]: + for x in range(w): + img.set_pixel(x, y, dark) + if y + 1 < h: + img.set_pixel(x, y + 1, dark) + + # Сучки + var knots: Array = [[18, 20], [42, 50], [25, 85], [48, 110]] + for k in knots: + for r in range(4): + for ang in range(12): + var a: float = float(ang) * PI / 6.0 + var px: int = k[0] + int(cos(a) * r) + var py: int = k[1] + int(sin(a) * r) + if px >= 0 and px < w and py >= 0 and py < h: + var t: float = 1.0 - (float(r) / 4.0) + img.set_pixel(px, py, base.lerp(knot, t)) + return _to_texture(img) ) -# Стол MICKE: белый с малозаметной шумной текстурой +# Стол MICKE: белый с минимальной шумной фактурой + наклейка-стикер func desk_white() -> ImageTexture: return _get_or_create("desk_white", func() -> ImageTexture: + var size: int = 64 var base: Color = Color(0.96, 0.95, 0.93) var nick: Color = Color(0.88, 0.86, 0.82) - var img: Image = _make_image(32, 32, base) - var rng: RandomNumberGenerator = RandomNumberGenerator.new() - rng.seed = 1337 - for i in range(8): - var x: int = rng.randi_range(0, 31) - var y: int = rng.randi_range(0, 31) - img.set_pixel(x, y, nick) + var img: Image = _make_image(size, size, base) + var noise: FastNoiseLite = _noise(1337, 0.18) + for y in range(size): + for x in range(size): + var n: float = noise.get_noise_2d(x, y) * 0.5 + 0.5 + img.set_pixel(x, y, base.lerp(nick, n * 0.08)) + # Стикер в углу — жёлтый Steam-inventory + var sticker: Color = Color(0.94, 0.71, 0.1) + for x in range(2, 8): + for y in range(2, 8): + img.set_pixel(x, y, sticker) + # Царапины + for x in range(20, 40): + img.set_pixel(x, 32, nick.darkened(0.1)) return _to_texture(img) ) -# Книжный корешок (несколько цветов) +# Книжный корешок — цветной с полосами func book_color(color_idx: int) -> ImageTexture: var key: String = "book_%d" % color_idx return _get_or_create(key, func() -> ImageTexture: @@ -248,22 +511,31 @@ func book_color(color_idx: int) -> ImageTexture: Color(0.3, 0.5, 0.3), ] var base: Color = palette[color_idx % palette.size()] - var trim: Color = Color(base.r * 0.6, base.g * 0.6, base.b * 0.6) - var img: Image = _make_image(16, 32, base) - # узкие линии-трим сверху и снизу - for x in range(16): + var trim: Color = base.darkened(0.4) + var hi: Color = base.lightened(0.2) + var w: int = 16 + var h: int = 64 + var img: Image = _make_image(w, h, base) + # Верхние/нижние полосы + for x in range(w): img.set_pixel(x, 0, trim) - img.set_pixel(x, 31, trim) - img.set_pixel(x, 5, trim) - img.set_pixel(x, 26, trim) + img.set_pixel(x, 1, trim) + img.set_pixel(x, h - 1, trim) + img.set_pixel(x, h - 2, trim) + img.set_pixel(x, 6, hi) + img.set_pixel(x, h - 7, hi) + # Тонкая текстура корешка + for y in range(8, h - 8): + img.set_pixel(0, y, trim) + img.set_pixel(w - 1, y, trim) return _to_texture(img) ) -# Применить текстуру к материалу с заданным uv-scale +# Применить текстуру к материалу с заданным uv-scale + nearest filter func apply_to_material(mat: BaseMaterial3D, texture: ImageTexture, scale: Vector3 = Vector3.ONE) -> void: if mat == null: return mat.albedo_texture = texture mat.uv1_scale = scale - mat.texture_filter = BaseMaterial3D.TEXTURE_FILTER_NEAREST # пиксель-арт + mat.texture_filter = BaseMaterial3D.TEXTURE_FILTER_NEAREST