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 — «Качели»
|
// ИГРА 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)`,
|
||||||
},
|
},
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user