Files
City10/resources/views/city/viewer.blade.php
T
marcocosta10000 090b530481 feat(city): add plaza exercise system with health tracking
- Adds CityPlazas support class for deterministic public plaza generation with outdoor gym equipment
- Implements exercise and stop-exercise actions with proximity validation to equipment
- Introduces health (0-100) and exercise_count attributes to Character model
- Reduces hospital stay duration from two game days to one game hour
- Adds 3D gym equipment rendering (bench, bars, bike, treadmill) in city viewer
- Includes exercise state machine handling in world ticker with energy drain
- Adds comprehensive PlazaExerciseTest and updates HospitalAdmissionTest
- Provides localized UI strings for health HUD and exercise prompts
2026-10-17 22:10:50 -03:00

291 lines
18 KiB
PHP
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@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>
{{-- Gym equipment proximity prompt --}}
<div id="exercise-prompt"
style="display:none; position:absolute; bottom:1.4rem; left:50%; transform:translateX(-50%);
z-index:20; background:rgba(20,83,45,.92); color:#bbf7d0; border:1px solid #4ade80;
border-radius:.6rem; padding:.5rem 1rem; font-size:.95rem; font-weight:600; white-space:nowrap;">
</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_health') }}: <span id="hud-health" style="color:#f9a8d4;">—</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>{{ __('city.hud_exercise') }}: <span id="hud-exercise-count" style="color:#c4b5fd;">—</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')),
exerciseUse: @json(__('city.exercise_use')),
exerciseActive: @json(__('city.exercise_active')),
exerciseStopHint: @json(__('city.exercise_stopHint')),
exerciseStart: @json(__('city.exerciseStart')),
exerciseStop: @json(__('city.exerciseStop')),
exerciseMaxHealth: @json(__('city.exercise_max_health')),
},
citySlug: @json($city->slug),
};
</script>
<script type="module" src="{{ asset('js/city/app.js') }}"></script>
@endpush