feat: 50 игр на Lua + импорт Roblox для всех + поддержка Lua в плеере #39

Merged
min merged 215 commits from feat/lua-50-games-bundle into main 2026-06-09 21:59:25 +00:00
3 changed files with 93 additions and 23 deletions
Showing only changes of commit 430b9eddcd - Show all commits

View File

@ -1563,25 +1563,75 @@ end)`,
// ИГРА 18 — «Качели» // ИГРА 18 — «Качели»
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
'swing': { 'swing': {
g18_main: `-- === ИГРА «КАЧЕЛИ» (Lua) === g18_main: `-- === ИГРА «КАЧЕЛИ» — главный скрипт (Lua) ===
local TweenService = game:GetService("TweenService") ${SNIPPET_BROADCAST}
local swing = workspace:WaitForChild("Качели")
local startPos = swing.Position
-- Качаем туда-сюда бесконечно local Players = game:GetService("Players")
task.spawn(function() local RunService = game:GetService("RunService")
while true do local player = Players.LocalPlayer
local up = TweenService:Create(swing, local won = false
TweenInfo.new(1, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut),
{ Position = startPos + Vector3.new(0, 0, 5) }) __rbxl_show_text("Запрыгни на качели и прокатись!", 3)
up:Play(); up.Completed:Wait()
local down = TweenService:Create(swing, local winSound = Instance.new("Sound", workspace)
TweenInfo.new(1, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut), winSound.SoundId = "win"; winSound.Volume = 1
{ Position = startPos + Vector3.new(0, 0, -5) })
down:Play(); down.Completed:Wait() -- Раскачиваем качели туда-сюда через изменение Position.Z.
-- WaitForChild может зависнуть берём напрямую с задержкой.
local swing = nil
local startZ = 0
task.delay(0.2, function()
swing = workspace:FindFirstChild("Качели")
if swing then
startZ = swing.Position.Z
end end
end) end)
print("Запрыгни на качающуюся платформу!")`,
local elapsed = 0
RunService.Heartbeat:Connect(function(dt)
if won then return end
if not swing then return end
elapsed = elapsed + (dt or 0.016)
-- Синусоидальное качание с амплитудой 4 и периодом ~2.8 сек
local amp = 4
local period = 2.8
local offsetZ = amp * math.sin(elapsed * 2 * math.pi / period)
local pos = swing.Position
swing.Position = Vector3.new(pos.X, pos.Y, startZ + offsetZ)
-- Если упал респаун
local py = __rbxl_player_y()
if py < -3 then
player:LoadCharacter()
end
end)
-- Финиш сообщает о победе
local winEvent = getEvent("WinReached")
winEvent.Event:Connect(function()
if won then return end
won = true
winSound:Play()
__rbxl_show_text("Победа! Ты перебрался на качелях!", 5)
local px = __rbxl_player_x()
local py = __rbxl_player_y()
local pz = __rbxl_player_z()
__rbxl_spawn_particles("confetti", px, py + 3, pz, 3, 3)
end)`,
g18_finish: `-- === Скрипт финиша (Lua) ===
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local part = script.Parent
local fired = false
part.Touched:Connect(function(hit)
if fired then return end
local h = hit.Parent and hit.Parent:FindFirstChild("Humanoid")
if not h then return end
fired = true
local ev = ReplicatedStorage:FindFirstChild("WinReached")
if ev then ev:Fire() end
end)`,
}, },
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════

View File

@ -25,8 +25,12 @@ export default function ConfirmModal({
cancelLabel = 'Отмена', cancelLabel = 'Отмена',
confirmTone = 'primary', // 'primary' | 'danger' confirmTone = 'primary', // 'primary' | 'danger'
onConfirm, onConfirm,
onCancel, // если задан вызывается при клике на «cancel» вместо тихого закрытия
onClose, onClose,
}) { }) {
const handleCancel = () => {
try { onCancel?.(); } finally { onClose?.(); }
};
const confirmBtnRef = useRef(null); const confirmBtnRef = useRef(null);
useEffect(() => { useEffect(() => {
@ -119,7 +123,7 @@ export default function ConfirmModal({
gap: 8, gap: 8,
}}> }}>
<button <button
onClick={onClose} onClick={handleCancel}
style={{ style={{
padding: '8px 16px', padding: '8px 16px',
borderRadius: 8, borderRadius: 8,

View File

@ -43,6 +43,7 @@ import KubikonDesktopOnlyStub from '../community/KubikonDesktopOnlyStub';
import KubikonBugReportButton from '../components/KubikonBugReport/KubikonBugReportButton'; import KubikonBugReportButton from '../components/KubikonBugReport/KubikonBugReportButton';
import cl from './KubikonEditor.module.css'; import cl from './KubikonEditor.module.css';
import Icon from './Icon'; import Icon from './Icon';
import ConfirmModal from './ConfirmModal';
const AUTOSAVE_DEBOUNCE_MS = 10000; // 10 секунд тишины авто-сохранение const AUTOSAVE_DEBOUNCE_MS = 10000; // 10 секунд тишины авто-сохранение
@ -512,6 +513,8 @@ const KubikonEditor = () => {
// BillboardEditorModal открывается из инспектора при клике // BillboardEditorModal открывается из инспектора при клике
// «Редактировать табличку». Содержит primitiveData выделенного билборда. // «Редактировать табличку». Содержит primitiveData выделенного билборда.
const [billboardEditorData, setBillboardEditorData] = useState(null); const [billboardEditorData, setBillboardEditorData] = useState(null);
// ConfirmModal кастомная модалка вместо window.confirm.
const [confirmState, setConfirmState] = useState(null);
// Bumper для обновления списков в Toolbox после edit/settings/delete. // Bumper для обновления списков в Toolbox после edit/settings/delete.
const [userModelsRefreshKey, setUserModelsRefreshKey] = useState(0); const [userModelsRefreshKey, setUserModelsRefreshKey] = useState(0);
// Bump-счётчик: инкрементируется при создании/очистке гладкого // Bump-счётчик: инкрементируется при создании/очистке гладкого
@ -2043,14 +2046,20 @@ const KubikonEditor = () => {
// Флаш ScriptEditor без этого 600мс свежих правок не успеют // Флаш ScriptEditor без этого 600мс свежих правок не успеют
// попасть в _scripts[]/dirtyRef и confirm-диалог не покажется. // попасть в _scripts[]/dirtyRef и confirm-диалог не покажется.
try { scriptEditorFlushRef.current?.(); } catch (_) {} try { scriptEditorFlushRef.current?.(); } catch (_) {}
// Несохранённые изменения спрашиваем // Несохранённые изменения кастомная модалка с 3 кнопками:
// Сохранить (по умолчанию), Не сохранять, Отмена.
if (dirtyRef.current) { if (dirtyRef.current) {
const ok = window.confirm('Есть несохранённые изменения. Сохранить перед выходом?'); setConfirmState({
if (ok) { title: 'Несохранённые изменения',
doSave().finally(() => navigate('/')); message: 'Сохранить проект перед выходом? Если выйти без сохранения — последние правки пропадут.',
confirmLabel: 'Сохранить и выйти',
cancelLabel: 'Выйти без сохранения',
confirmTone: 'primary',
onConfirm: () => doSave().finally(() => navigate('/')),
onCancel: () => navigate('/'), // выйти без сохранения
});
return; return;
} }
}
navigate('/'); navigate('/');
}; };
@ -4220,6 +4229,13 @@ const KubikonEditor = () => {
setBillboardEditorData(null); setBillboardEditorData(null);
}} }}
/> />
{/* Кастомная модалка подтверждения вместо window.confirm. */}
{confirmState && (
<ConfirmModal
{...confirmState}
onClose={() => setConfirmState(null)}
/>
)}
</div> </div>
); );
}; };