- Adds Character model with survival needs (hunger, thirst, energy) and state machine - Implements city:tick world ticker for offline progression every minute - Adds city territory expansion and building construction system - Introduces job economy, inventory, and transaction audit trail - Adds social graph (friends, marriage, children) and character chat - Implements admin approval workflow for new registrations - Adds hospital admission system for traffic accidents - Includes comprehensive feature tests and world seeders
276 lines
17 KiB
PHP
Executable File
276 lines
17 KiB
PHP
Executable File
@extends('layouts.app')
|
||
|
||
@section('title', __('city.title'))
|
||
|
||
@section('content')
|
||
<div style="max-width: 1280px; margin: 0 auto;">
|
||
<div style="display:flex; justify-content:space-between; align-items:center; gap:1rem; flex-wrap:wrap; margin-bottom:1rem;">
|
||
<h1 style="margin:0; color:#fff;">🏙️ {{ __('city.title') }}</h1>
|
||
<div style="display:flex; gap:.6rem; align-items:center; flex-wrap:wrap;">
|
||
<span id="hud-clock" style="background:rgba(15,23,42,.85); border:1px solid var(--border); border-radius:.5rem;
|
||
padding:.35rem .6rem; font-size:.85rem; color:#fde68a; font-variant-numeric:tabular-nums;">🕒 --:--</span>
|
||
<span id="hud-weather" style="background:rgba(15,23,42,.85); border:1px solid var(--border); border-radius:.5rem;
|
||
padding:.35rem .6rem; font-size:.85rem; color:#7dd3fc;">☀️ —</span>
|
||
<span id="stats" style="color:var(--muted); font-size:.9rem;"></span>
|
||
<button type="button" id="audio-toggle" class="btn" aria-label="{{ __('city.audio') }}" title="{{ __('city.audio') }}">🔊</button>
|
||
<button type="button" id="reload" class="btn">{{ __('city.refresh') }}</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="scene"
|
||
style="position:relative; width:100%; height:min(78vh, 720px); border-radius:1rem; overflow:hidden;
|
||
background:#0b1220; border:1px solid var(--border); box-shadow:0 20px 50px rgba(0,0,0,.45);">
|
||
<div id="loading" style="position:absolute; inset:0; display:grid; place-items:center; color:var(--muted);">
|
||
{{ __('city.loading') }}
|
||
</div>
|
||
<div id="error" style="position:absolute; inset:0; display:none; place-items:center; color:#fecaca;">
|
||
{{ __('city.error') }}
|
||
</div>
|
||
|
||
{{-- Hospital admission banner --}}
|
||
<div id="hospital-banner"
|
||
style="display:none; position:absolute; top:5.2rem; left:50%; transform:translateX(-50%);
|
||
z-index:20; background:rgba(127,29,29,.92); color:#fecaca; border:1px solid #f87171;
|
||
border-radius:.6rem; padding:.55rem 1rem; font-size:.95rem; font-weight:600;">
|
||
🏥
|
||
</div>
|
||
|
||
{{-- Character vitals and actions --}}
|
||
<div id="char-panel"
|
||
style="position:absolute; top:.7rem; left:.7rem; z-index:12; width:250px;
|
||
background:rgba(15,23,42,.93); border:1px solid var(--border);
|
||
border-radius:.75rem; padding:.75rem; font-size:.8rem; color:var(--muted);">
|
||
<div style="color:#fff; font-weight:700; margin-bottom:.4rem;" id="char-name">—</div>
|
||
<div>{{ __('city.hud_hunger') }}: <span id="hud-hunger" style="color:#fca5a5;">—</span></div>
|
||
<div>{{ __('city.hud_thirst') }}: <span id="hud-thirst" style="color:#7dd3fc;">—</span></div>
|
||
<div>{{ __('city.hud_energy') }}: <span id="hud-energy" style="color:#86efac;">—</span></div>
|
||
<div>{{ __('city.hud_money') }}: <span id="hud-money" style="color:#fde68a;">—</span></div>
|
||
<div>{{ __('city.hud_state') }}: <span id="hud-char-state" style="color:#c4b5fd;">—</span></div>
|
||
<div>{{ __('city.hud_education') }}: <span id="hud-education">—</span></div>
|
||
<div style="margin-top:.35rem; color:#cbd5e1;">{{ __('city.hud_inventory') }}: <span id="hud-inv">—</span></div>
|
||
|
||
<div style="margin-top:.6rem; color:#fff; font-weight:600;">{{ __('city.hud_actions') }}</div>
|
||
<div style="display:flex; flex-wrap:wrap; gap:.3rem; margin-top:.3rem;">
|
||
<button type="button" class="btn char-act" data-act="eat" style="padding:.25rem .45rem; font-size:.75rem;">{{ __('city.act_eat') }}</button>
|
||
<button type="button" class="btn char-act" data-act="drink" style="padding:.25rem .45rem; font-size:.75rem;">{{ __('city.act_drink') }}</button>
|
||
<button type="button" class="btn char-act" data-act="sleep" style="padding:.25rem .45rem; font-size:.75rem;">{{ __('city.act_sleep') }}</button>
|
||
<button type="button" class="btn char-act" data-act="wake" style="padding:.25rem .45rem; font-size:.75rem;">{{ __('city.act_wake') }}</button>
|
||
<button type="button" class="btn char-act" data-act="work" style="padding:.25rem .45rem; font-size:.75rem;">{{ __('city.act_work') }}</button>
|
||
<button type="button" class="btn char-act" data-act="stop-work" style="padding:.25rem .45rem; font-size:.75rem;">{{ __('city.act_stop') }}</button>
|
||
<button type="button" class="btn char-act" data-act="study" style="padding:.25rem .45rem; font-size:.75rem;">{{ __('city.act_study') }}</button>
|
||
<button type="button" class="btn char-act" data-act="buy" style="padding:.25rem .45rem; font-size:.75rem;">{{ __('city.act_buy') }}</button>
|
||
<button type="button" class="btn char-act" data-act="sell" style="padding:.25rem .45rem; font-size:.75rem;">{{ __('city.act_sell_v') }}</button>
|
||
<button type="button" class="btn char-act" data-act="build" style="padding:.25rem .45rem; font-size:.75rem;">{{ __('city.act_build') }}</button>
|
||
</div>
|
||
|
||
<div style="margin-top:.6rem; color:#fff; font-weight:600;">{{ __('city.hud_jobs') }}</div>
|
||
<select id="job-select" style="width:100%; margin-top:.25rem; padding:.3rem; background:#0b1220; color:var(--text); border:1px solid var(--border); border-radius:.4rem;">
|
||
<option value="">—</option>
|
||
</select>
|
||
<button type="button" id="job-apply" class="btn" style="margin-top:.3rem; width:100%; padding:.3rem; font-size:.75rem;">{{ __('city.act_apply') }}</button>
|
||
|
||
<div style="margin-top:.6rem; color:#fff; font-weight:600;">{{ __('city.hud_social') }}</div>
|
||
<select id="citizen-select" style="width:100%; margin-top:.25rem; padding:.3rem; background:#0b1220; color:var(--text); border:1px solid var(--border); border-radius:.4rem;">
|
||
<option value="">—</option>
|
||
</select>
|
||
<div style="display:flex; gap:.3rem; margin-top:.3rem; flex-wrap:wrap;">
|
||
<button type="button" id="soc-friend" class="btn" style="padding:.25rem .4rem; font-size:.75rem;">{{ __('city.act_friend') }}</button>
|
||
<button type="button" id="soc-marry" class="btn" style="padding:.25rem .4rem; font-size:.75rem;">{{ __('city.act_marry') }}</button>
|
||
<button type="button" id="soc-child" class="btn" style="padding:.25rem .4rem; font-size:.75rem;">{{ __('city.act_child') }}</button>
|
||
</div>
|
||
<div id="char-msg" style="margin-top:.4rem; min-height:1.1em; color:#93c5fd;"></div>
|
||
</div>
|
||
|
||
{{-- Audio mixer (music / SFX / master volumes) --}}
|
||
<div id="audio-panel"
|
||
style="position:absolute; top:.7rem; right:.7rem; z-index:12; width:220px; display:none;
|
||
background:rgba(15,23,42,.93); border:1px solid var(--border);
|
||
border-radius:.75rem; padding:.75rem; font-size:.8rem; color:var(--muted);">
|
||
<div style="color:#fff; font-weight:700; margin-bottom:.5rem;">🎵 {{ __('city.audio') }}</div>
|
||
|
||
<label style="display:flex; justify-content:space-between; gap:.4rem; margin-bottom:.15rem;">
|
||
<span>{{ __('city.vol_master') }}</span>
|
||
<span id="vol-master-out" style="color:#e2e8f0; font-variant-numeric:tabular-nums;">85%</span>
|
||
</label>
|
||
<input type="range" id="vol-master" min="0" max="1" step="0.01" value="0.85" style="width:100%;">
|
||
|
||
<label style="display:flex; justify-content:space-between; gap:.4rem; margin:.45rem 0 .15rem;">
|
||
<span>{{ __('city.vol_music') }}</span>
|
||
<span id="vol-music-out" style="color:#e2e8f0; font-variant-numeric:tabular-nums;">70%</span>
|
||
</label>
|
||
<input type="range" id="vol-music" min="0" max="1" step="0.01" value="0.7" style="width:100%;">
|
||
|
||
<label style="display:flex; justify-content:space-between; gap:.4rem; margin:.45rem 0 .15rem;">
|
||
<span>{{ __('city.vol_sfx') }}</span>
|
||
<span id="vol-sfx-out" style="color:#e2e8f0; font-variant-numeric:tabular-nums;">100%</span>
|
||
</label>
|
||
<input type="range" id="vol-sfx" min="0" max="1" step="0.01" value="1" style="width:100%;">
|
||
|
||
<button type="button" id="vol-mute" class="btn" style="margin-top:.55rem; width:100%; padding:.3rem;"
|
||
aria-label="{{ __('city.mute') }}">🔊</button>
|
||
<p style="margin:.45rem 0 0; font-size:.7rem; color:#64748b;">{{ __('city.audio_hint') }}</p>
|
||
</div>
|
||
|
||
{{-- Click-to-start overlay (pointer lock gate) --}}
|
||
<div id="start-gate"
|
||
style="position:absolute; inset:0; display:grid; place-items:center; background:rgba(11,18,32,.75);
|
||
cursor:pointer; z-index:10; text-align:center;">
|
||
<div>
|
||
<div style="font-size:3rem; margin-bottom:.5rem;">🎮</div>
|
||
<p style="color:#fff; font-size:1.1rem; margin:0 0 .3rem;">{{ __('city.hint_fps') }}</p>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- HUD bottom-left: POSIÇÃO panel (like example images) --}}
|
||
<div id="hud-pos"
|
||
style="position:absolute; left:.6rem; bottom:.6rem; z-index:5; display:none;
|
||
background:rgba(10,15,28,.88); border:1px solid rgba(148,163,184,.25); border-radius:.4rem;
|
||
padding:.5rem .7rem; font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:.72rem;
|
||
color:#94a3b8; line-height:1.55; min-width:170px; pointer-events:none;">
|
||
<div style="color:#e2e8f0; font-weight:700; letter-spacing:.06em; font-size:.65rem; margin-bottom:.2rem;">{{ __('city.position') }}</div>
|
||
<div id="pos-xyz">x:0.0 y:1.7 z:0.0</div>
|
||
<div id="pos-ground">—</div>
|
||
<div id="pos-tris">—</div>
|
||
</div>
|
||
|
||
{{-- HUD bottom-right: selected / hint --}}
|
||
<div id="hud-hint"
|
||
style="position:absolute; right:.6rem; bottom:.6rem; z-index:5; display:flex; gap:.5rem;
|
||
pointer-events:none; flex-wrap:wrap; justify-content:flex-end;">
|
||
<span id="selected" style="background:rgba(15,23,42,.85); border:1px solid var(--border); border-radius:.5rem;
|
||
padding:.35rem .6rem; font-size:.8rem; color:#7dd3fc; display:none;"></span>
|
||
<span id="hint" style="background:rgba(15,23,42,.85); border:1px solid var(--border); border-radius:.5rem;
|
||
padding:.35rem .6rem; font-size:.75rem; color:var(--muted);">{{ __('city.hint') }}</span>
|
||
</div>
|
||
|
||
{{-- Chat panel (bottom-right, like example images) --}}
|
||
<div id="chat-panel"
|
||
style="position:absolute; right:.6rem; bottom:3.2rem; z-index:15; width:280px; display:none;
|
||
background:rgba(15,23,42,.96); border:1px solid var(--border); border-radius:.6rem;
|
||
overflow:hidden; box-shadow:0 12px 30px rgba(0,0,0,.5); flex-direction:column;">
|
||
<div id="chat-header"
|
||
style="display:flex; align-items:center; justify-content:space-between; gap:.5rem;
|
||
padding:.55rem .7rem; background:rgba(30,41,59,.9); border-bottom:1px solid var(--border);">
|
||
<div style="display:flex; align-items:center; gap:.5rem; min-width:0;">
|
||
<span id="chat-dot" style="width:8px; height:8px; border-radius:50%; background:#22c55e; flex-shrink:0;"></span>
|
||
<div style="min-width:0;">
|
||
<div id="chat-name" style="font-weight:700; font-size:.85rem; color:#fff; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;"></div>
|
||
<div id="chat-status" style="font-size:.7rem; color:var(--muted);"></div>
|
||
</div>
|
||
</div>
|
||
<div style="display:flex; gap:.3rem;">
|
||
<span id="chat-time" style="font-size:.65rem; color:var(--muted); align-self:center;"></span>
|
||
<button type="button" id="chat-close"
|
||
style="background:transparent; border:none; color:var(--muted); cursor:pointer; font-size:1rem; padding:0 .2rem;"
|
||
aria-label="Close">×</button>
|
||
</div>
|
||
</div>
|
||
<div id="chat-messages"
|
||
style="padding:.6rem .7rem; overflow-y:auto; max-height:260px; display:flex; flex-direction:column;
|
||
gap:.5rem; font-size:.78rem; line-height:1.5;"></div>
|
||
<form id="chat-form"
|
||
style="display:flex; gap:.4rem; padding:.5rem .6rem; border-top:1px solid var(--border); background:rgba(11,18,32,.7);">
|
||
<input id="chat-input" type="text" maxlength="500" required
|
||
placeholder="{{ __('city.chat_placeholder') }}"
|
||
style="flex:1; min-width:0; padding:.45rem .6rem; border-radius:.4rem; border:1px solid var(--border);
|
||
background:#0b1220; color:var(--text); font-size:.8rem;">
|
||
<button type="submit" class="btn btn-primary" style="padding:.4rem .7rem; font-size:.75rem;">
|
||
{{ __('city.chat_send') }}
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
@endsection
|
||
|
||
@push('styles')
|
||
<style>
|
||
#scene canvas { display: block; touch-action: none; }
|
||
#chat-messages::-webkit-scrollbar { width: 6px; }
|
||
#chat-messages::-webkit-scrollbar-thumb { background: #334155; border-radius: 3px; }
|
||
.chat-bubble {
|
||
padding:.4rem .6rem; border-radius:.5rem; max-width:92%; word-break:break-word;
|
||
}
|
||
.chat-bubble.user {
|
||
background:rgba(14,165,233,.22); border:1px solid rgba(14,165,233,.4); color:#bae6fd; align-self:flex-end;
|
||
}
|
||
.chat-bubble.npc {
|
||
background:rgba(51,65,85,.55); border:1px solid rgba(148,163,184,.2); color:#e2e8f0; align-self:flex-start;
|
||
}
|
||
.chat-bubble.meta {
|
||
background:transparent; border:none; color:var(--muted); font-size:.7rem; align-self:center;
|
||
}
|
||
@media (max-width: 640px) {
|
||
#scene { height: 62vh !important; }
|
||
#hint { display: none; }
|
||
#chat-panel { width: calc(100% - 1.2rem); right: .6rem; left: .6rem; }
|
||
}
|
||
</style>
|
||
@endpush
|
||
|
||
@push('scripts')
|
||
<script>
|
||
window.CITY_CONFIG = {
|
||
dataUrl: @json(route('city.data')),
|
||
csrf: @json(csrf_token()),
|
||
i18n: {
|
||
stores: @json(__('city.stores_count')),
|
||
agents: @json(__('city.agents_count')),
|
||
vehicles: @json(__('city.vehicles_count')),
|
||
pedestrians: @json(__('city.pedestrians_count')),
|
||
open: @json(__('city.open')),
|
||
closed: @json(__('city.closed')),
|
||
selected: @json(__('city.selected')),
|
||
position: @json(__('city.position')),
|
||
onGround: @json(__('city.on_ground')),
|
||
tris: @json(__('city.triangles')),
|
||
chatThinking: @json(__('city.chat_thinking')),
|
||
chatResponding: @json(__('city.chat_responding')),
|
||
chatWith: @json(__('city.chat_with')),
|
||
chatSend: @json(__('city.chat_send')),
|
||
chatError: @json(__('city.chat_error')),
|
||
clock: @json(__('city.clock')),
|
||
weather: @json(__('city.weather')),
|
||
weather_clear: @json(__('city.weather_clear')),
|
||
weather_cloudy: @json(__('city.weather_cloudy')),
|
||
weather_rain: @json(__('city.weather_rain')),
|
||
weather_storm: @json(__('city.weather_storm')),
|
||
state_awake: @json(__('city.hud_state')),
|
||
actionOk: @json(__('city.action_ok')),
|
||
notFound: @json(__('city.action_unknown')),
|
||
jobs: @json(__('city.hud_jobs')),
|
||
applyJob: @json(__('city.apply_job')),
|
||
buyHint: @json(__('city.buy_hint')),
|
||
noFood: @json(__('city.no_food')),
|
||
noMoney: @json(__('city.no_money')),
|
||
noHouse: @json(__('city.no_house')),
|
||
noJob: @json(__('city.no_job')),
|
||
noMaterials: @json(__('city.no_materials')),
|
||
siteTaken: @json(__('city.site_taken')),
|
||
buildStarted: @json(__('city.build_started')),
|
||
outOfBounds: @json(__('city.out_of_bounds')),
|
||
socialFriendSent: @json(__('city.social_friend_sent')),
|
||
socialFriendOk: @json(__('city.social_friend_ok')),
|
||
socialMarriedOk: @json(__('city.social_married_ok')),
|
||
socialChildOk: @json(__('city.social_child_ok')),
|
||
chatSent: @json(__('city.chat_sent')),
|
||
me: @json(__('city.me')),
|
||
citizens: @json(__('city.citizens_near')),
|
||
hunger: @json(__('city.hud_hunger')),
|
||
thirst: @json(__('city.hud_thirst')),
|
||
energy: @json(__('city.hud_energy')),
|
||
money: @json(__('city.hud_money')),
|
||
education: @json(__('city.hud_education')),
|
||
inventory: @json(__('city.hud_inventory')),
|
||
hospitalHit: @json(__('city.hospital_hit')),
|
||
hospitalBlocked: @json(__('city.hospital_blocked')),
|
||
hospitalTitle: @json(__('city.hospital_title')),
|
||
hospitalLeft: @json(__('city.hospital_left')),
|
||
hospitalReleased: @json(__('city.hospital_released')),
|
||
},
|
||
citySlug: @json($city->slug),
|
||
};
|
||
</script>
|
||
<script type="module" src="{{ asset('js/city/app.js') }}"></script>
|
||
@endpush
|