/** * Сниппеты для Monaco-редактора скриптов (Фаза 6.1.3). * * Каждый сниппет — короткое имя (триггер) + готовый код с табстопами * (`${1:placeholder}`, `${2:...}`), который раскрывается по Tab. * * Регистрируется один раз через monaco.languages.registerCompletionItemProvider * для языка 'javascript' в ScriptEditor.jsx. * * Цель Фазы 6.1: новый юзер пишет первый скрипт и видит готовые шаблоны * на типичные задачи, не изобретая велосипед. */ export const SCRIPT_SNIPPETS = [ { label: 'on touch', detail: 'Игрок коснулся объекта (для скрипта на блоке/примитиве/модели)', body: [ "game.self.onTouch(() => {", "\t${1:// твой код когда игрок коснулся}", "\tgame.ui.showText('Привет!', 2);", "});", ], }, { label: 'on click', detail: 'Игрок кликнул по объекту (луч от прицела)', body: [ "game.self.onClick(() => {", "\t${1:// действие на клик}", "});", ], }, { label: 'on key', detail: 'Нажатие клавиши', body: [ "game.onKey('${1:e}', () => {", "\t${2:// действие на клавишу}", "});", ], }, { label: 'on tick', detail: 'Каждый кадр (для постоянной логики)', body: [ "game.onTick((dt) => {", "\t${1:// логика каждый кадр; dt — секунды с прошлого кадра}", "});", ], }, { label: 'tween', detail: 'Плавный переход свойств (движение, поворот, цвет)', body: [ "game.tween(${1:ref}, { ${2:y: 5} }, {", "\tduration: ${3:1},", "\teasing: '${4|ease,linear,bounce,elastic,back|}',", "\tonDone: () => { ${5:// после завершения} },", "});", ], }, { label: 'after', detail: 'Выполнить через N секунд', body: [ "game.after(${1:3}, () => {", "\t${2:game.ui.showText('Прошло 3 секунды', 2);}", "});", ], }, { label: 'every', detail: 'Повторять каждые N секунд', body: [ "const ${1:timerId} = game.every(${2:1}, () => {", "\t${3:// тик}", "});", "// game.cancel(${1:timerId}); — остановить", ], }, { label: 'door', detail: 'Дверь, открывающаяся по нажатию E', body: [ "// Скрипт привязать к двери. Клавиша E открывает.", "let isOpen = false;", "game.self.onInteract(() => {", "\tconst targetY = isOpen ? 0 : ${1:Math.PI / 2};", "\tgame.tween(game.self.ref, { rotationY: targetY }, { duration: 0.5, easing: 'ease' });", "\tisOpen = !isOpen;", "}, { text: isOpen ? 'Закрыть' : 'Открыть', distance: 4, key: 'e' });", ], }, { label: 'chest', detail: 'Сундук с наградой', body: [ "let opened = false;", "game.self.onInteract(() => {", "\tif (opened) return;", "\topened = true;", "\tgame.ui.showText('Ты получил ${1:сокровище}!', 2);", "\tgame.sound.play('coin');", "\tgame.economy.reward('${2:found_chest_1}');", "}, { text: 'Открыть сундук', key: 'e' });", ], }, { label: 'coin', detail: 'Монета — собирается и исчезает при касании', body: [ "game.self.onTouch(() => {", "\tgame.sound.play('coin');", "\tgame.ui.score = (game.ui.score || 0) + ${1:1};", "\tgame.self.delete();", "});", ], }, { label: 'spike', detail: 'Шип — наносит урон при касании', body: [ "game.self.onTouch(() => {", "\tgame.player.damage(${1:25});", "});", ], }, { label: 'portal', detail: 'Телепорт игрока в точку', body: [ "game.self.onTouch(() => {", "\tgame.player.teleport(${1:0}, ${2:5}, ${3:0});", "\tgame.sound.play('pickup');", "});", ], }, { label: 'npc', detail: 'Дружественный NPC с патрулём', body: [ "const ${1:trader} = game.scene.spawnNpc('${2:character-a}', {", "\tx: ${3:0}, y: ${4:1}, z: ${5:5},", "\tname: '${6:Торговец}',", "});", "${1:trader}.say('Привет, путник!', 3);", "// каждые 5 секунд идёт в случайную точку", "game.every(5, () => {", "\tconst x = game.random(-10, 10);", "\tconst z = game.random(-10, 10);", "\t${1:trader}.moveTo(x, z);", "});", ], }, { label: 'enemy', detail: 'Враг с патрулём вокруг точки', body: [ "const ${1:enemy} = game.scene.spawnNpc('${2:zombie}', {", "\tx: ${3:0}, y: ${4:1}, z: ${5:0},", "\thp: 50,", "});", "${1:enemy}.follow('player');", "${1:enemy}.onDeath(() => {", "\tgame.ui.showText('Враг повержен!', 1.5);", "\tgame.ui.score = (game.ui.score || 0) + 10;", "});", ], }, { label: 'quest', detail: 'Простой квест с шагами', body: [ "let step = 1;", "function showQuest() {", "\tconst steps = {", "\t\t1: 'Найди ${1:сундук}',", "\t\t2: 'Принеси ${2:торговцу}',", "\t\t3: 'Готово!',", "\t};", "\tgame.ui.set('quest', `Квест: ${steps[step] || ''}`, { x: 50, y: 10, color: '#ffd700' });", "}", "showQuest();", "// продвинуть шаг — game.broadcast('quest_step', { step: 2 });", "game.onMessage('quest_step', (data) => {", "\tstep = data.step;", "\tshowQuest();", "});", ], }, { label: 'save', detail: 'Сохранить прогресс игрока', body: [ "// Прочитать сохранение", "game.save.get('progress', (data) => {", "\tconst level = (data && data.level) || 1;", "\tgame.ui.showText(`Уровень: ${level}`, 2);", "});", "// Записать (после успеха)", "game.save.merge('progress', {", "\tincrement: { level: 1, coins: ${1:10} },", "\tmax: { bestTime: ${2:47.3} },", "});", ], }, { label: 'spawn', detail: 'Создать объект на сцене', body: [ "const ${1:obj} = game.scene.spawn('primitive:${2|cube,sphere,cylinder,cone|}', {", "\tx: ${3:0}, y: ${4:5}, z: ${5:0},", "\tcolor: '${6:#ff0000}',", "\tname: '${7:MyObject}',", "});", ], }, // === UI-шаблоны (Phase 6.3.5) === { label: 'ui-mainmenu', detail: 'Главное меню: заголовок + 3 кнопки по центру', body: [ "// Главное меню (Phase 6.3 шаблон)", "const menu = game.gui.create('frame', {", "\tname: 'MainMenu',", "\tx: 50, y: 50, w: 30, h: 50,", "\tanchor: 'center', anchorPoint: { x: 0.5, y: 0.5 },", "\tbgColor: '#1a1a2e', bgOpacity: 0.9,", "\tborderColor: '#4f74ff', borderWidth: 2, borderRadius: 12,", "\tlayout: 'vertical', layoutGap: 4, layoutPad: 5,", "});", "game.gui.create('text', {", "\tparentId: menu, text: '${1:Игра}',", "\ttextColor: '#ffd166', textSize: 28, fontWeight: 800, h: 15,", "});", "const playBtn = game.gui.create('button', { parentId: menu, text: 'Играть', h: 12 });", "const settBtn = game.gui.create('button', { parentId: menu, text: 'Настройки', h: 12 });", "const exitBtn = game.gui.create('button', { parentId: menu, text: 'Выйти', h: 12 });", "game.gui.onClick(playBtn, () => { game.gui.hide(menu); });", ], }, { label: 'ui-hp-bar', detail: 'HP-бар + патроны в углу (HUD)', body: [ "// HP-бар (Phase 6.3 шаблон)", "const hpBg = game.gui.create('frame', {", "\tname: 'HpBg', x: 2, y: 2, w: 22, h: 4, anchor: 'top-left',", "\tbgColor: '#222', bgOpacity: 0.8, borderRadius: 6,", "});", "const hpFill = game.gui.create('frame', {", "\tparentId: hpBg, x: 0, y: 0, w: 100, h: 100, anchor: 'top-left',", "\tanchorPoint: { x: 0, y: 0 },", "\tbgColor: '#22d97a', borderRadius: 6,", "});", "game.onHpChange(() => {", "\tconst pct = (game.player.hp / game.player.maxHp) * 100;", "\tgame.tween(hpFill, { w: pct }, { duration: 0.3, easing: 'ease' });", "});", ], }, { label: 'ui-shop', detail: 'Магазин со списком предметов (Grid)', body: [ "// Магазин со списком (Phase 6.3 шаблон)", "const shop = game.gui.create('frame', {", "\tname: 'Shop', x: 50, y: 50, w: 60, h: 70, anchor: 'center',", "\tbgColor: '#222', bgOpacity: 0.95, borderRadius: 12,", "\tlayout: 'grid', layoutCellW: 18, layoutCellH: 22, layoutCols: 4,", "\tlayoutGap: 2, layoutPad: 3,", "});", "const items = ${1:['меч', 'лук', 'щит', 'зелье', 'кольцо', 'плащ', 'броня', 'еда']};", "items.forEach((name) => {", "\tconst slot = game.gui.create('button', { parentId: shop, text: name, bgColor: '#3a3a5a' });", "\tgame.gui.onClick(slot, () => game.log('купил:', name));", "});", ], }, { label: 'ui-dialog', detail: 'Диалог NPC: рамка + текст + 2 кнопки', body: [ "// Диалог NPC (Phase 6.3 шаблон)", "const dlg = game.gui.create('frame', {", "\tname: 'Dialog', x: 50, y: 80, w: 60, h: 25, anchor: 'center',", "\tbgColor: '#1a1a2e', bgOpacity: 0.95, borderColor: '#ffd166', borderWidth: 2, borderRadius: 12,", "});", "game.gui.create('text', {", "\tparentId: dlg, x: 50, y: 25, w: 90, h: 40, anchor: 'center',", "\ttext: '${1:Привет, путник!}', textColor: '#fff', textSize: 18,", "});", "const yes = game.gui.create('button', {", "\tparentId: dlg, x: 30, y: 75, w: 30, h: 20, anchor: 'center', text: '${2:Да}',", "});", "const no = game.gui.create('button', {", "\tparentId: dlg, x: 70, y: 75, w: 30, h: 20, anchor: 'center', text: '${3:Нет}',", "});", "game.gui.onClick(yes, () => game.gui.remove(dlg));", "game.gui.onClick(no, () => game.gui.remove(dlg));", ], }, { label: 'ui-victory', detail: 'Экран победы с tween-появлением', body: [ "// Победа (Phase 6.3 шаблон)", "function showVictory(score) {", "\tconst panel = game.gui.create('frame', {", "\t\tname: 'Victory', x: 50, y: 50, w: 40, h: 30, anchor: 'center',", "\t\tbgColor: '#22d97a', bgOpacity: 0, borderRadius: 16,", "\t});", "\tgame.gui.create('text', {", "\t\tparentId: panel, x: 50, y: 30, w: 90, h: 30, anchor: 'center',", "\t\ttext: '\\u041f\\u041e\\u0411\\u0415\\u0414\\u0410!', textColor: '#fff', textSize: 36, fontWeight: 800,", "\t});", "\tgame.gui.create('text', {", "\t\tparentId: panel, x: 50, y: 70, w: 90, h: 20, anchor: 'center',", "\t\ttext: 'Счёт: ' + score, textColor: '#fff', textSize: 18,", "\t});", "\t// Tween-появление", "\tgame.tween(panel, { bgOpacity: 0.95 }, { duration: 0.5, easing: 'ease' });", "}", "showVictory(${1:100});", ], }, { label: 'ui-inventory', detail: 'Инвентарь 4×3 grid с tooltip', body: [ "// Инвентарь (Phase 6.3 шаблон)", "const inv = game.gui.create('frame', {", "\tname: 'Inventory', x: 50, y: 50, w: 50, h: 60, anchor: 'center',", "\tbgColor: '#222', bgOpacity: 0.95, borderRadius: 12,", "\tlayout: 'grid', layoutCellW: 22, layoutCellH: 30, layoutCols: 4,", "});", "for (let i = 0; i < ${1:12}; i++) {", "\tgame.gui.create('button', { parentId: inv, text: '[пусто]', bgColor: '#3a3a5a' });", "}", ], }, { label: 'ui-leaderboard', detail: 'Лидерборд: топ-N игроков', body: [ "// Лидерборд (Phase 6.3 шаблон)", "const lb = game.gui.create('frame', {", "\tname: 'Leaderboard', x: 98, y: 50, w: 22, h: 60, anchor: 'top-right',", "\tanchorPoint: { x: 1, y: 0.5 },", "\tbgColor: '#1a1a2e', bgOpacity: 0.9, borderRadius: 10,", "\tlayout: 'vertical', layoutGap: 1, layoutPad: 3,", "});", "game.gui.create('text', {", "\tparentId: lb, text: 'ТОП', textColor: '#ffd166', textSize: 18, h: 8, fontWeight: 800,", "});", "game.save.leaderboard.top('${1:score}', 10, (rows) => {", "\trows.forEach((r, i) => {", "\t\tgame.gui.create('text', {", "\t\t\tparentId: lb, h: 8,", "\t\t\ttext: `${i+1}. ${r.username}: ${r.value}`,", "\t\t\ttextColor: '#fff', textSize: 13, textAlign: 'left',", "\t\t});", "\t});", "});", ], }, { label: 'ui-loading', detail: 'Загрузочный экран с прогресс-баром', body: [ "// Загрузочный экран (Phase 6.3 шаблон)", "const overlay = game.gui.create('frame', {", "\tname: 'Loading', x: 50, y: 50, w: 100, h: 100, anchor: 'center',", "\tbgColor: '#000', bgOpacity: 0.85,", "});", "game.gui.create('text', {", "\tparentId: overlay, x: 50, y: 40, w: 60, h: 10, anchor: 'center',", "\ttext: '${1:Загрузка...}', textColor: '#ffd166', textSize: 24,", "});", "const barBg = game.gui.create('frame', {", "\tparentId: overlay, x: 50, y: 55, w: 40, h: 4, anchor: 'center',", "\tbgColor: '#222', borderRadius: 4,", "});", "const bar = game.gui.create('frame', {", "\tparentId: barBg, x: 0, y: 0, w: 0, h: 100, anchor: 'top-left',", "\tanchorPoint: { x: 0, y: 0 },", "\tbgColor: '#22d97a', borderRadius: 4,", "});", "// Tween-заполнение", "game.tween(bar, { w: 100 }, { duration: ${2:3}, onDone: () => game.gui.remove(overlay) });", ], }, { label: 'ui-settings', detail: 'Настройки: громкость + графика', body: [ "// Настройки (Phase 6.3 шаблон)", "const set = game.gui.create('frame', {", "\tname: 'Settings', x: 50, y: 50, w: 40, h: 50, anchor: 'center',", "\tbgColor: '#1a1a2e', bgOpacity: 0.95, borderRadius: 12,", "\tlayout: 'vertical', layoutGap: 3, layoutPad: 5,", "});", "game.gui.create('text', { parentId: set, text: 'Настройки', textColor: '#ffd166', textSize: 22, h: 12 });", "const volBtn = game.gui.create('button', { parentId: set, text: 'Звук: ВКЛ', h: 10 });", "const muted = { value: false };", "game.gui.onClick(volBtn, () => {", "\tmuted.value = !muted.value;", "\tgame.audio.setMuted(muted.value);", "\tgame.gui.update(volBtn, { text: muted.value ? 'Звук: ВЫКЛ' : 'Звук: ВКЛ' });", "});", "const closeBtn = game.gui.create('button', { parentId: set, text: 'Закрыть', h: 10 });", "game.gui.onClick(closeBtn, () => game.gui.remove(set));", ], }, { label: 'ui-card', detail: 'Карточка персонажа: аватар + ник + статы', body: [ "// Карточка персонажа (Phase 6.3 шаблон)", "const card = game.gui.create('frame', {", "\tname: 'Card', x: 2, y: 50, w: 18, h: 30, anchor: 'left',", "\tanchorPoint: { x: 0, y: 0.5 },", "\tbgColor: '#1a1a2e', bgOpacity: 0.9, borderRadius: 10,", "});", "game.gui.create('image', {", "\tparentId: card, x: 50, y: 25, w: 50, h: 40, anchor: 'center',", "\timageUrl: '${1:}', bgColor: '#3a3a5a',", "});", "game.gui.create('text', {", "\tparentId: card, x: 50, y: 60, w: 90, h: 12, anchor: 'center',", "\ttext: '${2:Стив}', textColor: '#fff', textSize: 16, fontWeight: 800,", "});", "game.gui.create('text', {", "\tparentId: card, x: 50, y: 80, w: 90, h: 12, anchor: 'center',", "\ttext: 'HP: ' + game.player.hp + ' / ' + game.player.maxHp,", "\ttextColor: '#22d97a', textSize: 13,", "});", ], }, { label: 'loading.transition', detail: 'Экран загрузки: переход на новый уровень (фейковый прогресс)', body: [ "await game.loading.transition({", "\tcover: { sceneSnapshot: true }, // снимок текущей сцены как превью", "\tduration: ${1:4}, // секунд заполняется бар", "\tskipButton: true,", "\tspinner: true,", "});", "${2:game.player.teleport(100, 1, 100);} // после загрузки", ], }, { label: 'loading.realProgress', detail: 'Экран загрузки: реальный прогресс подгрузки (ручной setProgress)', body: [ "const lo = game.loading.show({ progressBar: true, spinner: true });", "const total = ${1:10};", "let i = 0;", "const step = () => {", "\ti++;", "\t${2:// подгрузить i-й ресурс}", "\tlo.setProgress(i / total);", "\tif (i < total) game.after(${3:0.2}, step); else lo.close();", "};", "step();", ], }, { label: 'loading.minigame', detail: 'Экран загрузки: короткая пауза перед мини-игрой', body: [ "await game.loading.transition({", "\ttext: '${1:Загружаем мини-игру...}',", "\tduration: ${2:1.5},", "\tskipButton: false,", "});", "${3:// запустить мини-игру}", ], }, ]; /** * Регистрируем сниппеты в Monaco. Вызывается ОДИН раз для всего приложения * при первом монтировании редактора. */ export function registerSnippets(monaco) { if (monaco.__kubikonSnippetsRegistered) return; monaco.__kubikonSnippetsRegistered = true; monaco.languages.registerCompletionItemProvider('javascript', { triggerCharacters: [], provideCompletionItems(model, position) { const word = model.getWordUntilPosition(position); const range = { startLineNumber: position.lineNumber, endLineNumber: position.lineNumber, startColumn: word.startColumn, endColumn: word.endColumn, }; return { suggestions: SCRIPT_SNIPPETS.map((s) => ({ label: s.label, kind: monaco.languages.CompletionItemKind.Snippet, detail: s.detail, documentation: { value: '```js\n' + s.body.join('\n') + '\n```' }, insertText: s.body.join('\n'), insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, range, })), }; }, }); }