Начитавшись и насмотревшись как люди зарабатывают на яндекс играх, решил попробовать написать игру, совершенно не разбираясь в игроделываниии геймдеве. К тому же мне совершенно случайно попались видео с ютуба о том как использовать мощные LLM совершенно бесплатно. Ноль вложений.
Как я представлял себе процесс разработки: открываешь 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.vscodelaunch.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:Usersuser.qwenoauth_creds.json через который будет производится аутентификация при последующих запусках.
Скрытый текст

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

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

При этом в папке пользователя появляется файл C:Usersuser.geminioauth_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:UsersuserDownloadsgodotGodot_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:UsersuserDesktopProjectspuzzle`
Режимы ИИ агента
У Kilo Code есть несколько режимов работы.
Скрытый текст

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

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

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

Какая же эта игра без фоновой музыки? ИИ справился с добавлением фоновой музыки достаточно быстро.
Фальшстарт
И тут я решил запилить этот шедевр на яндекс игры. Зашел в консоль, на добавлял скриншотов с описанием игры. Ну и собственно загрузил игру экспортированную из 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
Фоновая музыка для игры – с бесплатной лицензией для использования в коммерческих целях.
Допиливание
Прежде чем выкладывать игру, надо в неё поиграть.
Ни у кого не получится очень точно, пиксель в пиксель разместить кусок пазла в нужное место на шаблоне с первого раза. Поэтому добавил авто-притягивание куска пазла если его положили близко к своему месту.
Новая проблема: все картинки разные, а шаблон – квадратный, поэтому при формировании пазла, картинка растягивается, что выглядит не очень. Нашел единственный вариант – дорисовывать по краям белый фон.
Белый фон по краям картинки

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

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

А на сегодня, всё…
Автор: virex


