Начитавшись и насмотревшись как люди зарабатывают на яндекс играх, решил попробовать написать игру, совершенно не разбираясь в игроделываниии геймдеве. К тому же мне совершенно случайно попались видео с ютуба о том как использовать мощные LLM (на минуточку, с контекстным окном в 1 миллион токенов!) совершенно бесплатно. Ноль вложений.
Как я представлял себе процесс разработки: открываешь IDE и пишешь "сделай игровую сцену, бла бла бла...", и через пару часов - готовый результат. Выкладываешь на яндекс игры, люди играют, реклама показывается - ты в плюсе.
Как выяснилось позже, никто не разрабатывает игру с нуля ...за редким исключением. В основном люди покупают в Сonstruct 3 готовые игры/шаблоны, перерисовывают картинки, добавляют уровни т.д.
Тяп, ляп, и в продакшн.
Но мы пойдем тернистым путём проб и ошибок.
На распутье
Итак. Нам нужно сделать игру для яндекс игр. Что такое яндекс игры? Это web страничка на html. Ок. Можно написать игру на html. Но мне хотелось как-то визуально править игровые ресурсы, через какой-нибудь редактор. Не писать же отдельно редактор для html игры?
Есть такая штука как "движок" игры: там можно вставлять свои ресурсы, писать логику, запускать/отлаживать игру. Какие есть игровые движки, без ограничений, бесплатные, с хорошей документацией, стабильные, существующие уже продолжительное время, с возможностью экспортировать игру в html5?
Таким идеальным движком мне показался Godot, с его почти полностью переведенной документацией. Тем более что я когда-то повторял игру по этим урокам на ютуб и экспортировал на android. Даже добавился в телеграмм канал и переписывался. Сейчас там целое сообщество. Но я совершенно забросил это дело, забыл напрочь всё что делал по урокам.
Сэт Ап
Соберем так называемое окружение для разработки.

Во первых скачаем и распакуем куда-нибудь сам Godot (Godot Engine – .NET качать не нужно т.к. насколько я понял, он не поддерживает экспорт в html5). Таким образом языком разработки у нас будет язык GDScript, чем то похожий на python.
Далее устанавливаем Visual Studio Code. Эта IDE будет основным "окном в разработку" игры.
Установим расширение godot-tools: заходим в File - Preferences - Extensions.
Далее в Visual Studio Code установим расширение Kilo Сode: вбиваем в поиск "kilocode.Kilo-Code". Жмем Install.
Скрытый текст

И тут важная вещь: необходимо откатиться на определенную версию этого расширения, а именно на 4.142.0 или ниже. Нажимаем на выпадающее меню около Uninstall и выбираем версию. Дело в том, что расширение выше этой версии, сломает нам работу на ИИ Gemini. Но об этом - позже.
Скрытый текст

Устанавливаем расширение Qwen Code Companion
Устанавливаем расширение Gemini CLI Companion
Устанавливаем NodeJS.
Запускаем командную строку cmd, далее в терминале вводим:
npm install -g @google/gemini-cli@latestТам же в терминале вводим:
npm install -g @qwen-code/qwen-code@latestСкачиваем и распаковываем Qdrant - он нужен Kilo Сode для работы с кодом. Можно скачать отсюда: qdrant-x86_64-pc-windows-msvc.zip.
Скачиваем LM Studio, скачиваем в нём модель nomic-embed-text-v2-moe-GGUF - она нужна для общения Kilo Сode и Qdrant между собой. Настраиваем LM Studio в качестве сервера. Как настраивать LM Studio в качестве сервера ИИ моделей можно прочитать в статье Открываем RAG и интернет для LM Studio (см. "Включаем сервер моделей в LM Studio").
Важное уточнение
Пока дописывал статью, Kilo Code совсем перестал работать с Gemini CLI, даже старые версии Kilo Code 4.142.0 и ниже перестали работать. Прощай бесплатная Gemini 2.5 Pro... Остается только Qwen3-coder-plus.

Хотя в Gemini CLI можно конечно работать и без Kilo Code, напрямую, в консольной утилите от гугла.
Настройки
Запускаем Godot вручную и создаем новый проект например в папке puzzle. Т.к. мы разрабатываем с последующим экспортом в html5, выбираем "Отрисовщик: Совместимость". После чего можно закрыть Godot.
Скрытый текст

В VSCode выбираем File - Open Folder..., указываем папку где только что создали проект: puzzle.
Godot-tools
Вообще изначально я предполагал что это расширение нужно для разработки, оно позволяет правильно разрисовывать код GDScript, подсвечивает ошибки и прочее в VSCode. Но если вам не нужно вручную ковыряться - можно и не настраивать.
Настройки
Заходим в File - Preferencesd - Settings, вводим godot в поиске, вводим путь до exe файла godot в Godot tools > Editor Path: Godot 4

Затем в VSCode нажимаем F1, вводим View: Show Run and Debug, далее create a launch.json file, выбираем GDScript Godot Debug.

Создается файл puzzle\.vscode\launch.json:
{ "version": "0.2.0", "configurations": [ { "name": "GDScript: Launch Project", "type": "godot", "request": "launch", "project": "${workspaceFolder}", "debug_collisions": false, "debug_paths": false, "debug_navigation": false, "additional_options": "" } ] }
Теперь можно нажать в правом нижнем углу Open workspace with Godot Editor, откроется Godot, и надпись с Disconnedted сменится на Connected

Qwen CLI
Для работы с бесплатной моделью нам необходимо зарегистрироваться https://chat.qwen.ai/auth?mode=register. Далее запускаем cmd, а в нем команда qwen. Выбираем 1 вариант, вас перекинет в окно где нужно войти под своей учеткой и вуаля. При этом в папке пользователя появляется файл C:\Users\user\.qwen\oauth_creds.json через который будет производится аутентификация при последующих запусках.
Скрытый текст

Gemini CLI
Необходимо зайти https://console.cloud.google.com, скопировать оттуда Project ID, и добавить его в системные или пользовательские переменные среды.
Скрытый текст

Далее запускаем в командной строке gemini, нас перекидывает на страницу авторизации, подтверждаем.
Скрытый текст

При этом в папке пользователя появляется файл C:\Users\user\.gemini\oauth_creds.json.
Kilo Code
Ну а теперь можно подключить наши модели к агенту с которым и будем далее работать.
Скрытый текст
Указываем русский язык.

Добавляем профиль для Qwen CLI

Указываем провайдер Qwen Code и файл аутентификации:

Для Gemini CLI так же создаем новое подключение и указываем настройки:

Так же для gemini я бы установил "Лимит скорости" около 7 секунд.

Зачем это нужно? Gemini при слишком частых обращениях может выдавать ошибку timeout (слишком частые запросы). Лимит скорости между запросами в 7 секунд - устраняет эту проблему.
Затем добавляем MCP серверы для того что бы ИИ агент мог обращаться к свежей документации по GDScript.

Нажимаем на "Редактировать глобальный MCP" и вставляем в файл такое содержимое:
{ "mcpServers":{ "context7":{ "type":"streamable-http", "url":"https://mcp.context7.com/mcp", "headers":{ "CONTEXT7_API_KEY":"xxx" }, "alwaysAllow":[ "query-docs", "resolve-library-id" ], "disabled":false, "timeout":15 }, "godot-docs":{ "type":"streamable-http", "url":"https://godot-docs-mcp.j2d.workers.dev/mcp", "alwaysAllow":[ "search_docs", "get_docs_page_for_term", "" ], "timeout":15, "disabled":false } } }
Для context7 нужно генерировать свой токен (CONTEXT7_API_KEY) у них на сайте. Хотя можно и без него.
Затем переключаемся на главное окно Kilo Code, нажимаем на значок индексации:

Указываем настройки подключения к Qdrant и к embedding модели обитающей на нашем LM Studio, жмем "Начать" - статус станет зелёным.

Создаем игру
Итак, язык GDScript не такой уж и распространенный в мире, что бы кодовая база попала в gemini и qwen когда их обучали. Поэтому нам желательно установить некие правила, по которым эти модели будут создавать игру. Для этого у Kilo Code есть настройка - мы можем дать моделям правила в markdown разметке с которыми они должны всегда сверятся.
Скрытый текст

Добавим файл rules.md:

Как написать такое содержимое - вопрос. Я попросил GPT 5 на https://arena.ai/ сформировать этот файл. Включил туда общие правила по языку GDScript и по сценам, плюс место откуда модель может запустить игру напрямую (если сказать что-то типа "запусти проект и проанализируй логи"). Правильно это или нет - я не знаю, но вроде получилось.
rules.md
You are an expert game developer specializing in **Godot Engine 4.4** (2D) on **Windows**, with strong knowledge of **typed GDScript**, scalable game architecture, debugging, and performance optimization. ## 0) Version Lock + Documentation Sources (Godot 4.4) - Target **Godot 4.4** APIs and behavior. Do **not** use Godot 3.x patterns. - Primary source of truth: official Godot **4.4** manual + class reference: `https://docs.godotengine.org/en/4.4/` - Always verify: - Node/class names, - property names, - signal names, - method signatures, - and lifecycle callbacks against the **4.4** docs. - If you must use **stable/latest** docs as a fallback, explicitly say so and re-check compatibility with 4.4. --- ## 1) Core Mission - Give **correct, production-ready** guidance for Godot 4.4 **2D** development. - Prefer **Godot built-ins** (Nodes/Scenes, Signals, InputMap, Resources, Animation, Physics, UI) over custom frameworks. - For each solution provide: - A short **plan** - **Implementation steps** (Editor + code) - **Complete runnable code** (or a clear patch when asked) - **Pitfalls** and **performance notes** --- ## 2) Writing Style (LLM-friendly) - Be **clear, technical, and concrete**. - Use short paragraphs, lists, and fenced code blocks. - Avoid vague advice; show **exact node names**, **file paths**, and **Godot 4.4 APIs**. - If multiple approaches exist: explain trade-offs and recommend one. --- ## 3) Godot 4 Architecture Rules (2D) - Use **Scenes** as reusable prefabs (`PackedScene`) for Player, Enemy, UI widgets, projectiles, pickups. - Prefer **composition over inheritance** (node/component-style). - Keep scene trees shallow and readable; name nodes clearly. - Avoid fragile parent-chains like `get_parent().get_parent()`; prefer signals, groups, or explicit references. --- ## 4) Node Selection (2D defaults) - `CharacterBody2D` — character movement (player/enemies). - `RigidBody2D` — physics-driven objects. - `Area2D` — triggers, hurtboxes/hitboxes, pickups. - `Control` — UI (don’t build UI on `Node2D`). --- ## 5) GDScript 2.0 (Godot 4.4) — MUST-KNOW LANGUAGE RULES ### 5.1 Indentation is semantic (Python-like) - Indentation defines blocks. Wrong indentation = wrong program. - Use **tabs** for indentation (Godot style guide). - Prefer readable multiline formatting; avoid overly dense one-liners. ### 5.2 Naming conventions (Godot style) - `PascalCase` — classes/types/nodes. - `snake_case` — variables, functions, signals. - `ALL_CAPS` — constants. - Prefer trailing commas in multiline arrays/dicts/enums for cleaner diffs. ### 5.3 Script lifecycle (Node callbacks — use correctly) - `_enter_tree()` — node enters the SceneTree (can happen multiple times). - `_ready()` — node + its children are in the SceneTree; children’s `_ready()` run **before** the parent; usually called only once per node lifetime. - `_process(delta)` — every rendered frame (variable timestep). - `_physics_process(delta)` — fixed timestep (physics loop); use for movement/collisions. - `_exit_tree()` — node leaves the SceneTree. ### 5.4 Input callbacks (priority matters) - Prefer `_unhandled_input(event)` for gameplay input so UI can consume events first. - Use `_shortcut_input(event)` for shortcuts; it runs before `_unhandled_key_input()` and `_unhandled_input()`. - Use polling (`Input.is_action_pressed`, `Input.get_vector`) for continuous movement in `_physics_process()`. ### 5.5 Initialization order (common bug source) Understand this order for Node-derived scripts: 1) member vars default init, 2) member var assignments top-to-bottom, 3) `_init()` runs (if defined), 4) exported values are assigned (when instancing scenes/resources), 5) `@onready` vars initialize, 6) `_ready()` runs. ### 5.6 `@onready` (defer node lookups safely) - `@onready` defers member initialization until `_ready()`. - Do NOT combine `@onready` with `@export` on the same variable (it causes confusing overrides and is treated as an error by default). Good: - `@export var speed: float = 300.0` - `@onready var sprite: Sprite2D = $Sprite2D` ### 5.7 Typed GDScript (required) - Use typed GDScript by default: - `var hp: int = 10` - `func take_damage(amount: int) -> void:` - Always specify return types, including `-> void`. - Prefer typed arrays/dicts where practical: - `var points: Array[Vector2] = []` - `var costs: Dictionary[String, int] = {"apple": 5}` - Use `:=` for type inference only when it’s truly obvious and improves readability. ### 5.8 Properties (setters/getters) — Godot 4 behavior - Use property syntax: var _ms: int = 0 var seconds: int: get: return _ms / 1000 set(value): _ms = value * 1000 - In Godot 4, `set`/`get` are called consistently even from inside the same class (with exceptions described in docs). - Avoid accidental infinite recursion when calling helper methods inside setters/getters. ### 5.9 Signals (decoupling rule) - Prefer signals for communication between systems (UI ↔ gameplay). - Signals are first-class values in Godot 4 (like `Callable`). - Prefer the recommended connection style using `Signal.connect()`: button.button_down.connect(_on_button_down) player.hit.connect(_on_player_hit.bind("sword", 100)) - Declare custom signals with `signal`, emit with `.emit(...)`. ### 5.10 `await` (coroutines by awaiting signals) - `await` is used to wait for signals (or other awaitables). - Canonical delay: await get_tree().create_timer(1.0).timeout - `SceneTree.create_timer()` returns a `SceneTreeTimer` that emits `timeout` and is auto-freed. - Use the `create_timer(..., process_in_physics=..., ignore_time_scale=...)` flags when you need precise timing behavior. ### 5.11 Tool scripts - Use `@tool` at the top to run script code in the editor. - Be careful with `queue_free()`/`free()` in tool scripts (can crash the editor). ### 5.12 Memory management (must be correct) - `RefCounted`-based objects (including `Resource`) free automatically when unreferenced. - `Node` is not ref-counted: free with `queue_free()` (preferred) or `free()`. --- ## 6) Exported Properties & Data - Use `@export` for tunables in Inspector. - Use export grouping when helpful: - `@export_group("Movement")` - `@export_subgroup("Air")` - Prefer **Resources** for data assets, not hard-coded dictionaries. - Use Autoload singletons sparingly and keep them thin: - `GameState`, `AudioManager`, `SceneLoader`, `SaveSystem` --- ## 7) Input (Godot Way) - Use **InputMap actions** (Project Settings → Input Map). - Prefer `StringName` literals for action names: - `Input.is_action_pressed(&"move_left")` - `Input.is_action_just_pressed(&"jump")` - Always list required InputMap actions in setup instructions. --- ## 8) UI Rules - Use `Control` + Containers (`VBoxContainer`, `HBoxContainer`, `MarginContainer`) for layout. - Avoid hard-coded pixel positioning when containers can solve it. - Prefer Themes for consistent UI styling. --- ## 9) Animation Rules - Use `AnimationPlayer` for timelines and simple animation control. - Use `AnimationTree` (state machine) for complex character animation logic. - Keep animation state changes explicit and debuggable. --- ## 10) Audio Rules - Use `AudioStreamPlayer`, `AudioStreamPlayer2D` - Use buses for volume groups (SFX/Music/UI) - Don’t recreate streams every time; reuse players or pool when needed. --- ## 11) Error Handling & Debugging (Windows) - Logging: `print()`, `push_warning()`, `push_error()`, `assert()` - Use Godot tools: Debugger, Remote Inspector, Profiler, Monitors - When errors are likely, include: - symptom - reproduction steps - fix - how to verify --- ## 12) Performance Rules (Godot 4.x) - Avoid per-frame allocations in hot paths. - Pool frequently spawned nodes (bullets/VFX). - Use collision layers/masks to reduce physics checks. - Use `queue_free()` responsibly; avoid mass churn every frame. - Profile first (Profiler + Monitors), then optimize. --- ## 13) Code Organization - Keep code well-organized with meaningful names. - Use doc comments `##` for class/module documentation and Inspector tooltips. - Keep functions small; comment only tricky logic. --- ## 14) Output Format (Hard Requirements) When you provide code, always include: - **File path**, e.g. `res://player/player.gd` - **Node tree expectations** - **Inspector settings** (`@export` values to set) - **Signal connections** (who emits, who listens) - **InputMap actions** needed - A short **"how to test"** checklist --- ## 15) Programming / Environment Specific (Windows) - Godot executable path (current): `C:\Users\user\Downloads\godot\Godot_v4.5.1-stable_win64_console.exe` - This ruleset targets **Godot 4.4** docs/APIs. Avoid using features introduced after 4.4 unless you verify compatibility. - Create project with Godot. - Do not close Godot during debugging. - Environment is Windows 11; cmd tools available for file/folder operations. - For basic puzzle mechanics (image slicing + snapping), you can use the project structure at: `C:\Users\user\Desktop\Projects\puzzle`
Режимы ИИ агента
У Kilo Code есть несколько режимов работы.
Скрытый текст

Архитектор - создает план по которому будет производить написание кода. Сам может переключиться в режим "Код".
Код - здесь у модели есть права на написание кода, чем собственно она и занимается.
Вопросы - тут можно задавать вопросы без изменения кода.
Отладка - в этом режиме можно почти бесконечно исправлять то, что отказывается работать.
Оркестратор - менеджер проекта, разбивает сложный запрос на подзадачи и распределяет их между предыдущими режимами.
Для любого режима можно выбрать модель. Обычно для Архитектора я выбираю Gemini, а для кода - Qwen.
Каждый режим можно отредактировать: поправить промпт, указать модель. По кнопке "Предпросмотр системного промпта" можно увидеть как Kilo Code внедряет в результативный промпт текст из rules.md.
Полезные вещи
Есть такая очень полезная вещь как "улучшение промпта" с учетом текущего проекта. Например вы пишете "добавь сцену с меню: уровень сложности, выход". Эта кнопка обогатит ваш запрос в более конкретный промпт с учетом текущей реализации.
Скрытый текст

Однако иногда надо вчитываться в то, как вам "улучшили" ваш запрос. Бывает что он может не правильно вас понять и вписать ненужную вам логику.
Очень удобная вещь в Kilo Code - это возможность восстановить произведенные изменения в проекте. Эта штука будет спасать вас не раз.
Скрытый текст

В начале было Слово
Как вы уже догадались, решил я "разработать" мозгами ИИ игру "Пазл". Вот прямо так ему и написал: "создай игру пазл. есть главное меню, и игровое поле с картинкой".
...спустя несколько минут часов это уже было похоже на:
Стартовый промпт
Создайте с нуля новую 2D-игру на Godot 4 под названием "Собрание Паззлов", предназначенную для Windows 11. Игра должна динамически загружать изображения из внешних источников, с первоначальной реализацией, использующей публичный API музея Метрополитен: https://collectionapi.metmuseum.org/public/collection/v1, выбирая случайный объект, имеющий доступное поле primaryImage. Стартовое меню игры должно предоставлять выбор источника изображений (по умолчанию Метрополитен) и три уровня сложности: Легкий (например, 3x3 кусочка), Средний (например, 5x5 кусочков) и Сложный (например, 7x7 кусочков), где количество кусочков увеличивается с возрастанием сложности. После нажатия кнопки "Старт" открывается основное игровое поле: в левом верхнем углу отображается полноразмерное превью выбранного изображения с его описанием (например, название, автор, дата), полученным из API; в центральной части экрана находится область, где пользователь собирает разбросанные кусочки паззла, которые должны иметь функцию перетаскивания и автоматического притягивания (снаппинга) к соседним правильным позициям; в правом верхнем углу отображается счетчик "Собрано: X" (количество корректно соединенных кусочков), и кнопка "Обновить", которая загружает новое изображение и генерирует новый паззл. Во время загрузки нового изображения должна отображаться анимированная индикация загрузки, а кнопка "Обновить" должна быть временно недоступна. Предусмотрите кнопку "Выйти" или возможность выхода по нажатию клавиши ESC; если паззл не завершен, перед выходом должно появиться диалоговое окно подтверждения "Действительно выйти?". Для реализации базовых механик паззла, таких как нарезка изображений и логика снаппинга, можно ориентироваться на структуру проекта по пути C:\Users\user\Documents\Jigsaw-Puzzle-2D-master\Jigsaw-Puzzle-2D-master.
Да пришлось найти где то пример пазла, который брал картинки из бесплатного публичного API музея "Метрополитен" из Нью-Йорка.
В этом примере не хватало главного - не было реализации фигурных вырезов каждого кусочка пазла. На реализацию которого у меня ушло примерно ...четыре дня. Тут уже пришлось напрячь извилины и придумать моему помощнику идею: на основе шаблона с прозрачными линиями - вырезать эти самые кусочки. Шаблон генерировал с помощью chat-gpt5 и других моделей на lmarena.ai. С генерацией тоже было много проблем. Пришлось самому править шаблоны вручную.
Шаблон

Какая же эта игра без фоновой музыки? ИИ справился с добавлением фоновой музыки достаточно быстро.
Yandex API
Да, кстати, для работы игры в яндекс играх необходимо использовать yandex api. Есть неофициальный плагин для Godot https://github.com/BasilYes/godot-yandex-games-sdk.
Необходимо только добавить пару строк в стартовую сцену:
Скрытый текст
func _ready() -> void: YandexSDK.init_game() YandexSDK.game_ready()
Яндекс требует что бы при сворачивании окна с игрой, либо переключении на другую вкладку, любые звуки в игре отключались. Но в плагине это реализовано так, что при потери фокуса текущей ноды - звук выключается. А если у вас в игре есть всплывающие диалоги - звук будет обрываться при показе любого диалога.
Немного поковырявшись, исправил на такое:
yandex_sdk.gd
# колбэк для JavaScript var _visibility_callback = JavaScriptBridge.create_callback(_on_web_visibility_change) func _ready(): # если работает yandex sdk if is_working(): var doc = JavaScriptBridge.get_interface("document") if doc: # системное событие смены видимости вкладки doc.addEventListener("visibilitychange", _visibility_callback) func _on_web_visibility_change(_args): var doc = JavaScriptBridge.get_interface("document") if doc.visibilityState == "hidden": _on_focus_exited() else: _on_focus_entered() # Обработчик события получения фокуса func _on_focus_entered() -> void: print("focus in signal") # Мутируем все шины for i in range(AudioServer.bus_count): if has_node("/root/SettingsSaves"): AudioServer.set_bus_mute(i, get_node("/root/SettingsSaves").load_mute_volume("Master")) else: AudioServer.set_bus_mute(i, false) # Обработчик события потери фокуса func _on_focus_exited() -> void: print("focus out signal") # Мутируем все шины for i in range(AudioServer.bus_count): AudioServer.set_bus_mute(i, true)
Фальшстарт
И тут я решил запилить этот шедевр на яндекс игры. Зашел в консоль, надобавлял скриншотов с описанием игры. Ну и собственно загрузил игру экспортированную из godot в html5.
Игра запустилась, но ничего не загружается. Оказывается для доступа игры к серверу музея, необходимо добавлять url в консоль и ждать когда его одобрят. Проходит день, доступ к сайту дают. Игра не может загрузить картинку для создания пазла. Смотрю в консоль - ошибки CORS.
Сделал некий CORS ретранслятор на https://dash.cloudflare.com/, так же добавил его.
Код ретранслятора, если кому интересно
export default { async fetch(request, env) { // Обработка OPTIONS запросов (CORS preflight) if (request.method === 'OPTIONS') { return new Response(null, { headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, OPTIONS', 'Access-Control-Allow-Headers': '*', 'Access-Control-Max-Age': '86400', 'Accept-Ranges': 'bytes' } }); } try { const url = new URL(request.url); // ПРАВИЛЬНО извлекаем параметр url с помощью URLSearchParams const rawUrl = request.url.split('url=')[1]; var targetUrlParam = decodeURIComponent(rawUrl); /* test return new Response(targetUrlParam, { status: 200, headers: { 'Content-Type': 'text/plain' } }); */ // АЛЬТЕРНАТИВНЫЙ СПОСОБ (если URLSearchParams не сработал): if (!targetUrlParam) { // Ручной парсинг для случаев, когда параметры сложные const queryString = url.search.substring(1); // Убираем '?' const params = queryString.split('&'); let extractedUrl = null; for (const param of params) { if (param.startsWith('url=')) { extractedUrl = decodeURIComponent(param.substring(4)); break; } } if (!extractedUrl) { return new Response('No URL parameter provided', { status: 400, headers: { 'Content-Type': 'text/plain' } }); } // Если нашли URL ручным способом, используем его targetUrlParam = extractedUrl; } if (!targetUrlParam) { return new Response('No URL parameter provided', { status: 400, headers: { 'Content-Type': 'text/plain' } }); } // Исправляем URL с пробелами и неправильным форматированием let cleanedUrl = targetUrlParam.trim() .replace(/\s*:\s*\/\s*\//g, '://') // Исправляем "https ://", "http : //", и т.д. .replace(/\s+/g, '%20'); // Заменяем пробелы на %20 // Дополнительная очистка от лишних параметров proxy // Убираем все после первого &, если это не часть целевого URL // Но сначала проверяем, есть ли в URL уже параметры (?) let finalUrl; try { // Пытаемся создать URL объект для валидации finalUrl = new URL(cleanedUrl); } catch (e) { // Если не удалось, пробуем дополнительные методы очистки if (cleanedUrl.includes('?') && cleanedUrl.includes('&')) { // Для URL с параметрами оставляем всё как есть finalUrl = cleanedUrl; } else { // Пытаемся найти начало реального URL const possibleProtocols = ['http://', 'https://']; let startIndex = -1; for (const protocol of possibleProtocols) { const index = cleanedUrl.toLowerCase().indexOf(protocol); if (index !== -1) { startIndex = index; break; } } if (startIndex !== -1) { finalUrl = cleanedUrl.substring(startIndex); } else { finalUrl = cleanedUrl; } } try { finalUrl = new URL(finalUrl); } catch (e2) { return new Response('Invalid URL format after cleaning: ' + e2.message, { status: 400, headers: { 'Content-Type': 'text/plain' } }); } } // Создаем заголовки для запроса const headers = new Headers(); // Передаем Range заголовок если есть const rangeHeader = request.headers.get('Range'); if (rangeHeader) { headers.set('Range', rangeHeader); } // Копируем важные заголовки const forwardedHeaders = ['User-Agent', 'Accept', 'Accept-Language', 'Referer']; forwardedHeaders.forEach(header => { const value = request.headers.get(header); if (value) headers.set(header, value); }); // Выполняем запрос к целевому URL const upstreamResponse = await fetch(finalUrl.toString(), { headers: headers, redirect: 'follow' }); // Создаем заголовки ответа const responseHeaders = new Headers(upstreamResponse.headers); // Сохраняем критические заголовки const preserveHeaders = ['Content-Range', 'Accept-Ranges', 'Content-Length', 'Content-Type', 'Cache-Control']; preserveHeaders.forEach(header => { const value = upstreamResponse.headers.get(header); if (value) responseHeaders.set(header, value); }); // Добавляем CORS заголовки responseHeaders.set('Access-Control-Allow-Origin', '*'); responseHeaders.set('Access-Control-Expose-Headers', '*'); responseHeaders.set('Accept-Ranges', 'bytes'); responseHeaders.set('Cross-Origin-Opener-Policy', 'same-origin'); responseHeaders.set('Cross-Origin-Embedder-Policy', 'require-corp'); // Обработка 206 Partial Content if (upstreamResponse.status === 206) { return new Response(upstreamResponse.body, { status: 206, statusText: 'Partial Content', headers: responseHeaders }); } // Для бинарных данных используем потоковую передачу const contentType = (responseHeaders.get('content-type') || '').toLowerCase(); /* const isBinary = contentType.startsWith('image/') || contentType.startsWith('video/') || contentType.startsWith('audio/') || contentType.includes('application/octet-stream'); */ return new Response(upstreamResponse.body, { status: upstreamResponse.status, headers: responseHeaders }); } catch (error) { console.error('Proxy error:', error); if (error.name === 'TypeError' && error.message.includes('fetch')) { return new Response('Failed to fetch target URL. Check if the URL is valid and accessible.', { status: 502, headers: { 'Content-Type': 'text/plain', 'Access-Control-Allow-Origin': '*' } }); } return new Response('Proxy Error: ' + error.message, { status: 500, headers: { 'Content-Type': 'text/plain', 'Access-Control-Allow-Origin': '*' } }); } } };
Проблему это не решило: были страшные тормоза при загрузке из игры. Хотя браузер прекрасно загружал картинки. ИИ гугла сказал что это может быть проблема с тем как godot скачивает данные: не умеет он качать сжатые куски как это делает браузер.
Появилась еще одна проблема: фоновая музыка прерывалась при работе с сетью.
Локализация
Ладно, если Магомет не идет к горе... то сделаем проще. Картинки будем хранить на яндекс диске.
Идем на https://oauth.yandex.ru/ добавляем новое "приложение" MuzeumPuzzle, запрашиваемые права - Яндекс.Диск REST API • Доступ к папке приложения на Диске.
Получаем токен: заходим через https://oauth.yandex.ru/authorize?response_type=token&client_id=xxx и сохраняем токен, этот токен нам нужен будет для работы с яндекс диском. Токен работает ровно 1 год с момента получения.
У нас есть целый полигон от яндекса для тестирования api. Можно создать например папку, и она появится по пути: https://disk.yandex.ru/client/disk/Приложения/MuzeumPuzzle.
Теперь легким движением руки закидываем в папку игры MuzeumPuzzle фото и их описание.
Сначала думал поискать какой нибудь клиент яндекс диска на js, но в итоге написал свой плагин для godot:
yandex_disk_service.gd
extends Node const BASE_URL = "https://cloud-api.yandex.net" # готовый токен на 1 год с доступом к папке приложения var token: String = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" var headers = ["User-Agent: Godot-Puzzle-Game/1.0", "Accept: */*"] # регистрация связана с появлением окна # при этом должно быть не больше 30 токенов # поэтому обойдемся готовым токеном на 1 год #func auth(client_id: String) -> void: # var response = await _make_request("https://oauth.yandex.ru/authorize?response_type=token&client_id=" + client_id) # push_error("YandexDisk: Ошибка получения ссылки (Code %d)" % response.code) func check_auth() -> bool: var response = await _yandex_make_request(BASE_URL + "/v1/disk/resources?path=app:/") if response.code == 200: return true else: return false ## Получение списка файлов и папок func get_files(path: String = "app:/") -> Array: var url = BASE_URL + "/v1/disk/resources?path=" + path.uri_encode() var response = await _yandex_make_request(url) if response.code == 200: var data = response.data if data is Dictionary and data.has("_embedded"): return data["_embedded"].get("items", []) push_error("YandexDisk: Ошибка получения списка файлов (Code %d)" % response.code) return [] ## Получение временной ссылки на скачивание файла func get_download_link(path: String = "app:/") -> String: var url = BASE_URL + "/v1/disk/resources/download?path=" + path.uri_encode() var response = await _yandex_make_request(url) if response.code == 200: return response.data.get("href", "") push_error("YandexDisk: Ошибка получения ссылки (Code %d)" % response.code) return "" ## Получение изображения func get_image(path: String = "app:/") -> Image: # получаем ссылку var link = await get_download_link(path) # скачиваем по ссылке var image_result = await _make_request(link, headers) # преобразуем в картинку if image_result.result == HTTPRequest.RESULT_SUCCESS: var image = Image.new() var err = image.load_jpg_from_buffer(image_result.body) if err != OK: err = image.load_png_from_buffer(image_result.body) if err == OK: return image print("WARNING: Не удалось загрузить файл: %s" % path) return null ## Получение изображения func get_text(path: String = "app:/") -> String: # получаем ссылку var link = await get_download_link(path) # скачиваем по ссылке var result = await _make_request(link, headers) # преобразуем в картинку if result.result == HTTPRequest.RESULT_SUCCESS: return result.body.get_string_from_utf8() print("WARNING: Не удалось загрузить или декодировать изображение: %s" % path) return "" ## Внутренний метод для выполнения HTTP-запросов func _yandex_make_request(url: String) -> Dictionary: var http = HTTPRequest.new() http.max_redirects = 5 add_child(http) var headers = [ "Authorization: OAuth " + token, "Accept: application/json" ] var err = http.request(url, headers, HTTPClient.METHOD_GET) if err != OK: http.queue_free() return {"code": 0, "data": {}} var result = await http.request_completed # result[0] - result (int), result[1] - response_code (int), # result[2] - headers (PackedStringArray), result[3] - body (PackedByteArray) var response_code = result[1] var body = result[3].get_string_from_utf8() var json = JSON.new() var parse_err = json.parse(body) var data = json.get_data() if parse_err == OK else {} http.queue_free() return {"code": response_code, "data": data} ## обычный запрос func _make_request(url: String, headers: PackedStringArray) -> Dictionary: # Вместо использования одного глобального http_request, # создаем временный для этого конкретного вызова. # Это исключит ошибку 5 (ERR_ALREADY_IN_USE). var http = HTTPRequest.new() add_child(http) # Настраиваем TLS ДО вызова request #var tls_settings = TLSOptions.client_unsafe() #http.set_tls_options(tls_settings) http.timeout = 15 # Установим таймаут 15 секунд для предотвращения зависания http.max_redirects = 5 # Увеличиваем лимиты для бинарных данных http.body_size_limit = 26214400 # 25 МБ (чтобы точно влезли любые фото) var error = http.request(url, headers, HTTPClient.METHOD_GET, "") if error != OK: print("ERROR: %s" % error) http.queue_free() return {"result": -1} var result = await http.request_completed # Удаляем временный узел http.queue_free() return { "result": result[0], "code": result[1], "headers": result[2], "body": result[3] }
Звук
Для звука так же пытался найти а затем сделать отдельный worker, но так ничего не получилось. Сейчас точно не помню, но проблема была решена изменениями в двух местах: в экспорте в html5 нужно было убрать галку "Поддержка потоков". А в коде нужно было указать такое:
Фрагмент MusicService.gd
audio_player = AudioStreamPlayer.new() if OS.has_feature("web"): audio_player.playback_type = AudioServer.PLAYBACK_TYPE_SAMPLE else: audio_player.playback_type = AudioServer.PLAYBACK_TYPE_STREAM
Фоновая музыка для игры - с бесплатной лицензией для использования в коммерческих целях.
Допиливание
Прежде чем выкладывать игру, надо в неё поиграть.
Ни у кого не получится очень точно, пиксель в пиксель разместить кусок пазла в нужное место на шаблоне с первого раза. Поэтому добавил авто-притягивание куска пазла если его положили близко к своему месту.
Новая проблема: все картинки разные, а шаблон - квадратный, поэтому при формировании пазла, картинка растягивается, что выглядит не очень. Нашел единственный вариант - дорисовывать по краям белый фон.
Белый фон по краям картинки

Было не удобно когда сгенерированные кусочки раскидывались по всему игровому полю. Поэтому решил сделать некий "конвейер" с кусочками, откуда можно их доставать, не захламляя игровое поле.
При запуске игры на компьютере все идеально, но при запуске на телефоне - всё мелкое и шаблон очень мелкий. После долгих препираний с ИИ, удалось таки сделать динамическое масштабирование при изменении размеров игрового поля.
Разное масштабирование

Пиши пропало
И тут случилось то, чего я не ожидал. Kilo code перестал работать с gemini. Возврат агента на старую версию не намного отсрочил проблему. Через какое то время gemini перестала работать даже со старой версией агента.
Попробовал последние правки игры делать сразу в Qwen CLI, без VSCode. Общие впечатления: отвечает намного шустрее. Но работа через Kilo code намного нагляднее и проще, особенно с возвратом кода.
Итого
Это увлекательный был аттракцион...
Вайб прости господи кодинг по началу вызывает ощущение безграничных возможностей. Но эти возможности кардинально ограничены тем, кто решил нанять ИИ в качестве джуна. После многочисленных "да вы правы, сейчас исправим", понимаешь, что без собственных знаний в предмете, такое "программирование" начинает очень сильно утомлять.
История...


Какова ценность того, что сделал ИИ для вас, если вы сами в этом ничего не понимаете? Как оценить то, что написал для вас ИИ? Что с этим делать? Как это дальше использовать?
Вопрос - открытый.
Раньше все ругали «индусский код». Теперь на смену ему пришел куда более опасный ИИ-слоп.

А на сегодня всё...
