feat: игра 18 «Качели» Lua + кастомная модалка выхода
Игра 18: - Падало на WaitForChild + task.spawn + Completed:Wait (yield-across-C). - Полный переписан: Heartbeat-таймер раскачивает swing по синусу с амплитудой 4 и периодом 2.8с (вместо TweenService). - task.delay 0.2с чтобы дождаться появления свинга в scene. - Vector3.new напрямую (без оператора + который не работает). - BindableEvent WinReached + g18_finish.Touched → ev:Fire. - Полный паритет: showText, win Sound, confetti, respawn при y<-3. Модалка выхода: - Заменил window.confirm в KubikonEditor.handleBack на ConfirmModal. - 3 варианта: Сохранить и выйти / Выйти без сохранения / Отмена. - ConfirmModal расширен onCancel prop (отделяет 'cancel-кнопка' от 'клик мимо/Escape').
This commit is contained in:
parent
b5ba62cca8
commit
430b9eddcd
@ -1563,25 +1563,75 @@ end)`,
|
||||
// ИГРА 18 — «Качели»
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
'swing': {
|
||||
g18_main: `-- === ИГРА «КАЧЕЛИ» (Lua) ===
|
||||
local TweenService = game:GetService("TweenService")
|
||||
local swing = workspace:WaitForChild("Качели")
|
||||
local startPos = swing.Position
|
||||
g18_main: `-- === ИГРА «КАЧЕЛИ» — главный скрипт (Lua) ===
|
||||
${SNIPPET_BROADCAST}
|
||||
|
||||
-- Качаем туда-сюда бесконечно
|
||||
task.spawn(function()
|
||||
while true do
|
||||
local up = TweenService:Create(swing,
|
||||
TweenInfo.new(1, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut),
|
||||
{ Position = startPos + Vector3.new(0, 0, 5) })
|
||||
up:Play(); up.Completed:Wait()
|
||||
local down = TweenService:Create(swing,
|
||||
TweenInfo.new(1, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut),
|
||||
{ Position = startPos + Vector3.new(0, 0, -5) })
|
||||
down:Play(); down.Completed:Wait()
|
||||
local Players = game:GetService("Players")
|
||||
local RunService = game:GetService("RunService")
|
||||
local player = Players.LocalPlayer
|
||||
local won = false
|
||||
|
||||
__rbxl_show_text("Запрыгни на качели и прокатись!", 3)
|
||||
|
||||
local winSound = Instance.new("Sound", workspace)
|
||||
winSound.SoundId = "win"; winSound.Volume = 1
|
||||
|
||||
-- Раскачиваем качели туда-сюда через изменение 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)
|
||||
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)`,
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
@ -25,8 +25,12 @@ export default function ConfirmModal({
|
||||
cancelLabel = 'Отмена',
|
||||
confirmTone = 'primary', // 'primary' | 'danger'
|
||||
onConfirm,
|
||||
onCancel, // если задан — вызывается при клике на «cancel» вместо тихого закрытия
|
||||
onClose,
|
||||
}) {
|
||||
const handleCancel = () => {
|
||||
try { onCancel?.(); } finally { onClose?.(); }
|
||||
};
|
||||
const confirmBtnRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
@ -119,7 +123,7 @@ export default function ConfirmModal({
|
||||
gap: 8,
|
||||
}}>
|
||||
<button
|
||||
onClick={onClose}
|
||||
onClick={handleCancel}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
borderRadius: 8,
|
||||
|
||||
@ -43,6 +43,7 @@ import KubikonDesktopOnlyStub from '../community/KubikonDesktopOnlyStub';
|
||||
import KubikonBugReportButton from '../components/KubikonBugReport/KubikonBugReportButton';
|
||||
import cl from './KubikonEditor.module.css';
|
||||
import Icon from './Icon';
|
||||
import ConfirmModal from './ConfirmModal';
|
||||
|
||||
const AUTOSAVE_DEBOUNCE_MS = 10000; // 10 секунд тишины → авто-сохранение
|
||||
|
||||
@ -512,6 +513,8 @@ const KubikonEditor = () => {
|
||||
// BillboardEditorModal — открывается из инспектора при клике
|
||||
// «Редактировать табличку…». Содержит primitiveData выделенного билборда.
|
||||
const [billboardEditorData, setBillboardEditorData] = useState(null);
|
||||
// ConfirmModal — кастомная модалка вместо window.confirm.
|
||||
const [confirmState, setConfirmState] = useState(null);
|
||||
// Bumper для обновления списков в Toolbox после edit/settings/delete.
|
||||
const [userModelsRefreshKey, setUserModelsRefreshKey] = useState(0);
|
||||
// Bump-счётчик: инкрементируется при создании/очистке гладкого
|
||||
@ -2043,14 +2046,20 @@ const KubikonEditor = () => {
|
||||
// Флаш ScriptEditor — без этого 600мс свежих правок не успеют
|
||||
// попасть в _scripts[]/dirtyRef и confirm-диалог не покажется.
|
||||
try { scriptEditorFlushRef.current?.(); } catch (_) {}
|
||||
// Несохранённые изменения — спрашиваем
|
||||
// Несохранённые изменения — кастомная модалка с 3 кнопками:
|
||||
// Сохранить (по умолчанию), Не сохранять, Отмена.
|
||||
if (dirtyRef.current) {
|
||||
const ok = window.confirm('Есть несохранённые изменения. Сохранить перед выходом?');
|
||||
if (ok) {
|
||||
doSave().finally(() => navigate('/'));
|
||||
setConfirmState({
|
||||
title: 'Несохранённые изменения',
|
||||
message: 'Сохранить проект перед выходом? Если выйти без сохранения — последние правки пропадут.',
|
||||
confirmLabel: 'Сохранить и выйти',
|
||||
cancelLabel: 'Выйти без сохранения',
|
||||
confirmTone: 'primary',
|
||||
onConfirm: () => doSave().finally(() => navigate('/')),
|
||||
onCancel: () => navigate('/'), // выйти без сохранения
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
@ -4220,6 +4229,13 @@ const KubikonEditor = () => {
|
||||
setBillboardEditorData(null);
|
||||
}}
|
||||
/>
|
||||
{/* Кастомная модалка подтверждения вместо window.confirm. */}
|
||||
{confirmState && (
|
||||
<ConfirmModal
|
||||
{...confirmState}
|
||||
onClose={() => setConfirmState(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user