支持所有游戏按房间范围配置和运行

This commit is contained in:
pllx
2026-04-29 14:37:28 +08:00
parent 3672140987
commit 1607f57e3c
37 changed files with 1033 additions and 255 deletions
+30 -1
View File
@@ -9,6 +9,7 @@ let fishToken = null;
let autoFishing = false;
let autoFishCooldownTimer = null;
let autoFishCooldownCountdown = null;
let fishingCastPending = false;
/**
* 读取 CSRF Token。
@@ -199,6 +200,25 @@ function setFishingButton(text, disabled) {
button.disabled = disabled;
}
/**
* 判断当前是否已有进行中的钓鱼会话。
*
* 说明:
* - 手动点击抛竿后,在等待浮漂、等待点击、等待自动收竿期间都视为会话未结束。
* - 购买自动钓鱼卡后的自动接管,也必须避开这些中间态,避免重复抛竿。
*
* @returns {boolean}
*/
function hasActiveFishingSession() {
return Boolean(
fishingCastPending ||
fishToken ||
fishingTimer ||
fishingReelTimeout ||
document.getElementById("fishing-bobber"),
);
}
/**
* 启动自动钓鱼冷却倒计时(基于时间戳,不受浏览器后台节流影响)。
*
@@ -363,6 +383,11 @@ function hideAutoFishStopButton() {
* @returns {Promise<void>}
*/
export async function startFishing() {
if (hasActiveFishingSession()) {
return;
}
fishingCastPending = true;
setFishingButton("🎣 抛竿中...", true);
try {
@@ -381,6 +406,7 @@ export async function startFishing() {
return;
}
// 抛竿成功后进入正式钓鱼会话,由 token / timer 接管后续状态。
fishToken = data.token;
autoFishing = Boolean(data.auto_fishing);
appendFishingMessage(`<span style="color:#2563eb;font-weight:bold;">🎣【钓鱼】</span>${escapeHtml(data.message)}<span class="msg-time">(${timeText()})</span>`);
@@ -426,6 +452,8 @@ export async function startFishing() {
window.chatDialog?.alert?.(`网络错误:${error.message}`, "网络异常", "#cc4444");
removeBobber();
setFishingButton("🎣 钓鱼", false);
} finally {
fishingCastPending = false;
}
}
@@ -547,6 +575,7 @@ function clearAutoFishingTimers() {
*/
export function resetFishingBtn() {
autoFishing = false;
fishingCastPending = false;
clearAutoFishingTimers();
hideAutoFishStopButton();
@@ -573,7 +602,7 @@ export function checkAndAutoStartFishing() {
const minutesLeft = Number(window.chatContext?.autoFishingMinutesLeft || 0);
const initialCooldown = Number(window.chatContext?.fishingCooldownSeconds || 0);
if (minutesLeft <= 0 || autoFishing) {
if (minutesLeft <= 0 || autoFishing || hasActiveFishingSession()) {
return;
}
@@ -5,7 +5,6 @@
@section('content')
@php
$riddleTypeOptions = \App\Models\Riddle::typeOptions();
$availableRooms = \App\Models\Room::orderBy('id')->get();
@endphp
<div class="space-y-6">
@@ -88,6 +87,7 @@
{{-- 参数配置区域 --}}
<div class="p-5">
<form action="{{ route('admin.game-configs.params', $game) }}" method="POST"
data-game-room-form
@if ($game->game_key === 'idiom') data-idiom-config-form @endif>
@csrf
@@ -97,8 +97,9 @@
$hiddenLegacyKeys = $game->game_key === 'mystery_box'
? ['min_reward', 'max_reward', 'rare_min_reward', 'rare_max_reward']
: [];
$hiddenConfigKeys = ['room_scope_mode', 'room_ids'];
$paramKeys = array_values(array_unique(array_merge(array_keys($labels), array_keys($params))));
$paramKeys = array_values(array_filter($paramKeys, fn ($key) => ! in_array($key, $hiddenLegacyKeys, true)));
$paramKeys = array_values(array_filter($paramKeys, fn ($key) => ! in_array($key, array_merge($hiddenLegacyKeys, $hiddenConfigKeys), true)));
@endphp
@if ($game->game_key === 'idiom')
@@ -108,6 +109,9 @@
'riddleTypeOptions' => $riddleTypeOptions,
])
@else
@php
$roomScopeConfig = gameRoomScopeConfig($params);
@endphp
<div class="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4">
@foreach ($paramKeys as $paramKey)
@php
@@ -155,6 +159,14 @@
</div>
@endforeach
</div>
<div class="mt-4 border-t border-gray-100 pt-4">
@include('admin.game-configs.partials.common-room-scope', [
'availableRooms' => $availableRooms,
'roomScopeConfig' => $roomScopeConfig,
'roomScopeTitle' => '参与房间',
])
</div>
@endif
<div class="mt-4 flex items-center gap-3">
@@ -189,7 +201,7 @@
style="padding:8px 16px; background:linear-gradient(135deg,#7f1d1d,#ef4444); color:#fff; border:none; border-radius:8px; font-size:13px; font-weight:700; cursor:pointer; transition:opacity .15s;">
☠️ 投放黑化箱
</button>
<span class="text-xs text-gray-400">直接向 #1 房间投放,立即广播暗号</span>
<span class="text-xs text-gray-400">投放到当前配置的首选房间,立即广播暗号。</span>
</div>
</div>
@endif
@@ -461,4 +473,16 @@
->all(),
];
}
/**
* 解析通用游戏的房间范围配置。
*
* @return array{room_scope_mode:string,room_ids:array<int, int>}
*/
function gameRoomScopeConfig(array $params): array
{
$roomScopeService = app(\App\Services\GameRoomScopeService::class);
return $roomScopeService->getScopeConfigForParams($params);
}
@endphp
@@ -0,0 +1,79 @@
@php
$roomScopeConfig = $roomScopeConfig ?? ['room_scope_mode' => 'single', 'room_ids' => [1]];
$roomScopeDataKey = $roomScopeDataKey ?? 'game';
$roomScopeTitle = $roomScopeTitle ?? '参与房间';
$roomScopeCheckedRoomIds = collect($roomScopeConfig['room_ids'] ?? [1])->map(fn ($roomId) => (int) $roomId)->all();
@endphp
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
<div>
<label class="mb-1 block text-xs font-bold text-gray-600">参与房间模式</label>
<select name="params[room_scope_mode]"
data-game-room-mode
class="w-full rounded-lg border border-gray-300 p-2 text-sm focus:border-indigo-400">
<option value="all" @selected(($roomScopeConfig['room_scope_mode'] ?? 'single') === 'all')>全部房间</option>
<option value="single" @selected(($roomScopeConfig['room_scope_mode'] ?? 'single') === 'single')>单选房间</option>
<option value="multiple" @selected(($roomScopeConfig['room_scope_mode'] ?? 'single') === 'multiple')>多选房间</option>
</select>
</div>
<div class="md:col-span-2">
<label class="mb-2 block text-xs font-bold text-gray-600">{{ $roomScopeTitle }}</label>
<div class="grid grid-cols-2 gap-2 rounded-xl border border-dashed border-slate-200 bg-white p-3 lg:grid-cols-3">
@foreach ($availableRooms as $room)
<label class="flex items-center gap-2 rounded-lg border border-slate-100 px-3 py-2 text-sm text-slate-600">
<input type="checkbox"
name="params[room_ids][]"
value="{{ $room->id }}"
@checked(in_array((int) $room->id, $roomScopeCheckedRoomIds, true))
data-game-room-checkbox
class="rounded border-slate-300 text-indigo-600">
<span>#{{ $room->id }} {{ $room->name }}</span>
</label>
@endforeach
</div>
<p class="mt-2 text-xs text-gray-400">单选模式下只保留一个房间,多选模式可同时勾选多个房间。</p>
</div>
</div>
@once
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function () {
function showAdminAlert(message, title = '提示', icon = '️') {
if (window.adminDialog?.alert) {
window.adminDialog.alert(message, title, icon);
return;
}
window.alert(message);
}
document.querySelectorAll('[data-game-room-form]').forEach(function (form) {
form.addEventListener('submit', function (event) {
const modeSelect = this.querySelector('[data-game-room-mode]');
const roomCheckboxes = Array.from(this.querySelectorAll('[data-game-room-checkbox]'));
const checkedRooms = roomCheckboxes.filter(function (checkbox) {
return checkbox.checked;
});
if (!modeSelect) {
return;
}
if (modeSelect.value === 'single' && checkedRooms.length > 1) {
event.preventDefault();
showAdminAlert('单选房间模式下只能选择一个房间。', '房间选择有误', '⚠️');
return;
}
if (modeSelect.value === 'multiple' && checkedRooms.length === 0) {
event.preventDefault();
showAdminAlert('多选房间模式下,请至少选择一个房间。', '房间选择有误', '⚠️');
}
});
});
});
</script>
@endpush
@endonce
@@ -1,6 +1,5 @@
@php
$sharedConfig = gameRiddleSharedConfig($params);
$checkedRoomIds = collect($sharedConfig['room_ids'])->map(fn ($roomId) => (int) $roomId)->all();
@endphp
<div class="space-y-4">
@@ -53,33 +52,17 @@
class="w-full rounded-lg border border-gray-300 p-2 text-sm focus:border-indigo-400">
<p class="mt-1 text-xs text-gray-400">分钟,0 表示仅手动出题。</p>
</div>
<div>
<label class="mb-1 block text-xs font-bold text-gray-600">参与房间模式</label>
<select name="params[room_scope_mode]"
data-idiom-room-mode
class="w-full rounded-lg border border-gray-300 p-2 text-sm focus:border-indigo-400">
<option value="all" @selected($sharedConfig['room_mode'] === 'all')>全部房间</option>
<option value="single" @selected($sharedConfig['room_mode'] === 'single')>单选房间</option>
<option value="multiple" @selected($sharedConfig['room_mode'] === 'multiple')>多选房间</option>
</select>
</div>
<div class="xl:col-span-2">
<label class="mb-2 block text-xs font-bold text-gray-600" data-idiom-room-label>参与房间</label>
<div class="grid grid-cols-2 gap-2 rounded-xl border border-dashed border-slate-200 bg-white p-3 lg:grid-cols-3">
@foreach ($availableRooms as $room)
<label class="flex items-center gap-2 rounded-lg border border-slate-100 px-3 py-2 text-sm text-slate-600">
<input type="checkbox"
name="params[room_ids][]"
value="{{ $room->id }}"
@checked(in_array((int) $room->id, $checkedRoomIds, true))
data-idiom-room-checkbox
class="rounded border-slate-300 text-indigo-600">
<span>#{{ $room->id }} {{ $room->name }}</span>
</label>
@endforeach
</div>
<p class="mt-2 text-xs text-gray-400">单选模式下只保留一个房间,多选模式可同时勾选多个房间。</p>
</div>
</div>
<div class="mt-4 border-t border-gray-100 pt-4">
@include('admin.game-configs.partials.common-room-scope', [
'availableRooms' => $availableRooms,
'roomScopeConfig' => [
'room_scope_mode' => $sharedConfig['room_mode'],
'room_ids' => $sharedConfig['room_ids'],
],
'roomScopeTitle' => '参与房间',
])
</div>
</div>
</div>
@@ -121,41 +104,6 @@
window.alert(message);
}
document.querySelectorAll('[data-idiom-config-form]').forEach(function (form) {
form.addEventListener('submit', function (event) {
let hasValidationError = false;
const modeSelect = this.querySelector('[data-idiom-room-mode]');
const roomCheckboxes = Array.from(this.querySelectorAll('[data-idiom-room-checkbox]'));
const checkedRooms = roomCheckboxes.filter(function (checkbox) {
return checkbox.checked;
});
if (modeSelect) {
if (modeSelect.value === 'single' && checkedRooms.length > 1) {
showAdminAlert('猜谜活动处于单选房间模式时,只能勾选一个房间。', '房间选择有误', '⚠️');
hasValidationError = true;
}
if (modeSelect.value === 'single' && checkedRooms.length === 0) {
const firstRoomCheckbox = roomCheckboxes[0];
if (firstRoomCheckbox) {
firstRoomCheckbox.checked = true;
}
}
if (modeSelect.value === 'multiple' && checkedRooms.length === 0) {
showAdminAlert('猜谜活动处于多选房间模式时,请至少选择一个房间。', '房间选择有误', '⚠️');
hasValidationError = true;
}
}
if (hasValidationError) {
event.preventDefault();
}
});
});
const manualStartButton = document.getElementById('idiom-manual-start-btn');
if (!manualStartButton) {
return;