574 lines
14 KiB
Vue
574 lines
14 KiB
Vue
<template>
|
|
<div class="retro-container">
|
|
<!-- 标题栏 -->
|
|
<div class="title-bar">
|
|
<div class="title-text">[ VIRTUAL PET SYSTEM v1.0 ]</div>
|
|
<div class="system-status">{{ systemStatus }}</div>
|
|
</div>
|
|
|
|
<!-- 主游戏区域 -->
|
|
<div class="game-area" v-if="petState">
|
|
<!-- 状态显示区 -->
|
|
<div class="stats-panel">
|
|
<div class="panel-title">[ STATUS ]</div>
|
|
<div class="stat-row">
|
|
<span class="stat-name">NAME:</span>
|
|
<span class="stat-value">{{ petState.name || 'UNNAMED' }}</span>
|
|
</div>
|
|
<div class="stat-row">
|
|
<span class="stat-name">STAGE:</span>
|
|
<span class="stat-value">{{ getStageName(petState.stage) }}</span>
|
|
</div>
|
|
<div class="stat-row">
|
|
<span class="stat-name">AGE:</span>
|
|
<span class="stat-value">{{ formatAge(petState.ageSeconds) }}</span>
|
|
</div>
|
|
|
|
<div class="divider">---------------</div>
|
|
|
|
<div class="stat-row">
|
|
<span class="stat-name">HUNGER:</span>
|
|
<span class="stat-bar-text">{{ makeBar(petState.hunger) }} {{ Math.round(petState.hunger) }}</span>
|
|
</div>
|
|
<div class="stat-row">
|
|
<span class="stat-name">HAPPY:</span>
|
|
<span class="stat-bar-text">{{ makeBar(petState.happiness) }} {{ Math.round(petState.happiness) }}</span>
|
|
</div>
|
|
<div class="stat-row">
|
|
<span class="stat-name">HEALTH:</span>
|
|
<span class="stat-bar-text">{{ makeBar((petState.health / getMaxHealth(petState)) * 100) }} {{ Math.round(petState.health) }}/{{ getMaxHealth(petState) }}</span>
|
|
</div>
|
|
|
|
<div class="divider">---------------</div>
|
|
|
|
<div class="stat-row">
|
|
<span class="stat-name">STR:</span>
|
|
<span class="stat-value">{{ Math.round(petState.effectiveStr || petState.str || 0) }}</span>
|
|
</div>
|
|
<div class="stat-row">
|
|
<span class="stat-name">INT:</span>
|
|
<span class="stat-value">{{ Math.round(petState.effectiveInt || petState.int || 0) }}</span>
|
|
</div>
|
|
<div class="stat-row">
|
|
<span class="stat-name">DEX:</span>
|
|
<span class="stat-value">{{ Math.round(petState.effectiveDex || petState.dex || 0) }}</span>
|
|
</div>
|
|
<div class="stat-row">
|
|
<span class="stat-name">LUCK:</span>
|
|
<span class="stat-value">{{ Math.round(petState.effectiveLuck || petState.luck || 0) }}</span>
|
|
</div>
|
|
|
|
<div class="divider">---------------</div>
|
|
|
|
<div v-if="petState.isSleeping" class="status-flag">[SLEEPING]</div>
|
|
<div v-if="petState.isSick" class="status-flag sick">[SICK]</div>
|
|
<div v-if="petState.poopCount > 0" class="status-flag">[POOP x{{ petState.poopCount }}]</div>
|
|
</div>
|
|
|
|
<!-- 命令输入区 -->
|
|
<div class="command-panel">
|
|
<div class="panel-title">[ ACTIONS ]</div>
|
|
<div class="command-grid">
|
|
<button class="cmd-btn" @click="handleFeed" :disabled="!canDoAction('feed')">
|
|
[F]EED
|
|
</button>
|
|
<button class="cmd-btn" @click="handlePlay('normal')" :disabled="!canDoAction('play')">
|
|
[P]LAY
|
|
</button>
|
|
<button class="cmd-btn" @click="handleClean" :disabled="!canDoAction('clean')">
|
|
[C]LEAN
|
|
</button>
|
|
<button class="cmd-btn" @click="handleHeal" :disabled="!canDoAction('heal')">
|
|
[H]EAL
|
|
</button>
|
|
<button class="cmd-btn" @click="handleSleep" :disabled="!canDoAction('sleep')">
|
|
{{ petState?.isSleeping ? '[W]AKE' : '[S]LEEP' }}
|
|
</button>
|
|
<button class="cmd-btn" @click="handlePray" :disabled="!isReady">
|
|
P[R]AY
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 控制台输出 -->
|
|
<div class="console-panel">
|
|
<div class="panel-title">[ CONSOLE ]</div>
|
|
<div class="console-log" id="console-output">
|
|
<div class="log-line">SYSTEM READY...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Debug 面板 -->
|
|
<button class="debug-toggle" @click="showDebug = !showDebug">[DEBUG]</button>
|
|
<div v-if="showDebug" class="debug-panel">
|
|
<div class="panel-title">[ DEBUG TOOLS ]</div>
|
|
<button class="cmd-btn" @click="debugResetPrayer">RESET PRAYER</button>
|
|
<button class="cmd-btn" @click="debugAddFavor(50)">+50 FAVOR</button>
|
|
<button class="cmd-btn" @click="debugMaxAttributes">MAX STATS</button>
|
|
<button class="cmd-btn" @click="debugAddPoop">ADD POOP x4</button>
|
|
</div>
|
|
|
|
<!-- 名字输入 Modal -->
|
|
<div v-if="showNameInput" class="name-modal">
|
|
<div class="modal-box">
|
|
<div class="panel-title">[ NAME YOUR PET ]</div>
|
|
<input
|
|
v-model="inputPetName"
|
|
@keyup.enter="confirmName"
|
|
type="text"
|
|
placeholder="ENTER NAME..."
|
|
maxlength="20"
|
|
class="name-input"
|
|
/>
|
|
<button class="cmd-btn" @click="confirmName" :disabled="!inputPetName || inputPetName.trim().length === 0">
|
|
CONFIRM
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
import { PetSystem } from '../core/pet-system.js'
|
|
import { EventSystem } from '../core/event-system.js'
|
|
import { TempleSystem } from '../core/temple-system.js'
|
|
|
|
// 状态
|
|
const petState = ref(null)
|
|
const systemStatus = ref('INITIALIZING...')
|
|
const showDebug = ref(false)
|
|
const showNameInput = ref(false)
|
|
const inputPetName = ref('')
|
|
const isReady = ref(false)
|
|
const isRunning = ref(false)
|
|
|
|
// 系统实例
|
|
let petSystem = null
|
|
let eventSystem = null
|
|
let templeSystem = null
|
|
|
|
// 生成 ASCII 进度条
|
|
function makeBar(value) {
|
|
const MAX_BARS = 10
|
|
const filled = Math.round((value || 0) / 10)
|
|
return '[' + '█'.repeat(filled) + '░'.repeat(MAX_BARS - filled) + ']'
|
|
}
|
|
|
|
// 格式化年龄
|
|
function formatAge(seconds) {
|
|
if (!seconds) return '0s'
|
|
const h = Math.floor(seconds / 3600)
|
|
const m = Math.floor((seconds % 3600) / 60)
|
|
const s = Math.floor(seconds % 60)
|
|
if (h > 0) return `${h}h${m}m`
|
|
if (m > 0) return `${m}m${s}s`
|
|
return `${s}s`
|
|
}
|
|
|
|
// 获取阶段名称
|
|
function getStageName(stage) {
|
|
const names = {
|
|
egg: 'EGG',
|
|
baby: 'BABY',
|
|
child: 'CHILD',
|
|
adult: 'ADULT'
|
|
}
|
|
return names[stage] || stage
|
|
}
|
|
|
|
// 获取最大健康
|
|
function getMaxHealth(state) {
|
|
if (!state) return 100
|
|
const bonuses = getAllBonuses(state)
|
|
return 100 + (bonuses.health || 0)
|
|
}
|
|
|
|
// 获取加成
|
|
function getAllBonuses(state) {
|
|
if (!petSystem) return {}
|
|
return petSystem.getAllBonuses()
|
|
}
|
|
|
|
// 动作检查
|
|
function canDoAction(action) {
|
|
if (!petState.value) return false
|
|
if (action === 'feed') return petState.value.hunger < 100
|
|
if (action === 'play') return petState.value.hunger > 0 && petState.value.happiness < 100
|
|
if (action === 'clean') return petState.value.poopCount > 0
|
|
if (action === 'heal') return petState.value.health < getMaxHealth(petState.value)
|
|
if (action === 'sleep') return !petState.value.isDead
|
|
return isReady.value
|
|
}
|
|
|
|
// 动作处理
|
|
async function handleFeed() {
|
|
const result = await petSystem.feed()
|
|
log(result.success ? `FED PET. HUNGER +${result.hungerGain}` : result.message)
|
|
updateState()
|
|
}
|
|
|
|
async function handlePlay(type = 'normal') {
|
|
const result = await petSystem.play({ gameType: type })
|
|
log(result.success ? `PLAYED. HAPPY +${Math.round(result.happinessGain)}` : result.message)
|
|
updateState()
|
|
}
|
|
|
|
async function handleClean() {
|
|
const result = await petSystem.cleanPoop()
|
|
log(result.success ? `CLEANED ${result.poopCount} POOP` : result.message)
|
|
updateState()
|
|
}
|
|
|
|
async function handleHeal() {
|
|
const result = await petSystem.heal()
|
|
log(result.success ? `HEALED +${Math.round(result.healAmount)}` : result.message)
|
|
updateState()
|
|
}
|
|
|
|
async function handleSleep() {
|
|
const result = await petSystem.toggleSleep()
|
|
log(result.success ? result.message : result.message)
|
|
updateState()
|
|
}
|
|
|
|
async function handlePray() {
|
|
const result = await templeSystem.pray()
|
|
if (result.success) {
|
|
log(`PRAYED. FAVOR +${result.favorIncrease} (${result.newFavor}/100)`)
|
|
if (result.reachedMax) log('*** MAX FAVOR REACHED! ***')
|
|
} else {
|
|
log(result.message)
|
|
}
|
|
updateState()
|
|
}
|
|
|
|
// Debug 工具
|
|
async function debugResetPrayer() {
|
|
templeSystem.debugResetDailyPrayer()
|
|
log('[DEBUG] PRAYER RESET')
|
|
updateState()
|
|
}
|
|
|
|
async function debugAddFavor(amount) {
|
|
const deityId = petState.value.currentDeityId
|
|
const current = petState.value.deityFavors[deityId] || 0
|
|
await petSystem.updateState({
|
|
deityFavors: {
|
|
...petState.value.deityFavors,
|
|
[deityId]: Math.min(100, current + amount)
|
|
}
|
|
})
|
|
petSystem.calculateCombatStats()
|
|
log(`[DEBUG] FAVOR +${amount}`)
|
|
updateState()
|
|
}
|
|
|
|
async function debugMaxAttributes() {
|
|
await petSystem.debugSetStat('str', 100)
|
|
await petSystem.debugSetStat('int', 100)
|
|
await petSystem.debugSetStat('dex', 100)
|
|
log('[DEBUG] STATS MAXED')
|
|
updateState()
|
|
}
|
|
|
|
async function debugAddPoop() {
|
|
await petSystem.debugSetStat('poopCount', 4)
|
|
log('[DEBUG] ADDED 4 POOP')
|
|
updateState()
|
|
}
|
|
|
|
// 更新状态
|
|
function updateState() {
|
|
if (petSystem) {
|
|
petState.value = { ...petSystem.getState() }
|
|
}
|
|
}
|
|
|
|
// 日志
|
|
function log(message) {
|
|
const consoleEl = document.getElementById('console-output')
|
|
if (consoleEl) {
|
|
const line = document.createElement('div')
|
|
line.className = 'log-line'
|
|
line.textContent = `> ${message}`
|
|
consoleEl.appendChild(line)
|
|
consoleEl.scrollTop = consoleEl.scrollHeight
|
|
|
|
// 限制日志行数
|
|
while (consoleEl.children.length > 50) {
|
|
consoleEl.removeChild(consoleEl.firstChild)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 确认名字
|
|
async function confirmName() {
|
|
if (inputPetName.value && inputPetName.value.trim().length > 0) {
|
|
await petSystem.updateState({ name: inputPetName.value.trim() })
|
|
showNameInput.value = false
|
|
log(`PET NAMED: ${inputPetName.value.trim()}`)
|
|
updateState()
|
|
}
|
|
}
|
|
|
|
// 初始化
|
|
onMounted(async () => {
|
|
log('LOADING SYSTEMS...')
|
|
|
|
petSystem = new PetSystem()
|
|
eventSystem = new EventSystem(petSystem)
|
|
templeSystem = new TempleSystem(petSystem)
|
|
|
|
await petSystem.initialize()
|
|
await eventSystem.initialize()
|
|
|
|
updateState()
|
|
|
|
// 检查是否有名字
|
|
if (!petState.value.name) {
|
|
showNameInput.value = true
|
|
}
|
|
|
|
// 启动循环
|
|
petSystem.startTickLoop()
|
|
eventSystem.startEventCheck()
|
|
isRunning.value = true
|
|
isReady.value = true
|
|
|
|
systemStatus.value = 'RUNNING'
|
|
log('SYSTEM INITIALIZED')
|
|
log('TYPE COMMANDS TO INTERACT')
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
if (petSystem) petSystem.stopTickLoop()
|
|
if (eventSystem) eventSystem.stopEventCheck()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
|
|
|
|
* {
|
|
font-family: 'Press Start 2P', monospace;
|
|
image-rendering: pixelated;
|
|
}
|
|
|
|
.retro-container {
|
|
background: #000;
|
|
color: #0f0;
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
font-size: 12px;
|
|
line-height: 1.8;
|
|
}
|
|
|
|
.title-bar {
|
|
border: 2px solid #0f0;
|
|
padding: 10px;
|
|
margin-bottom: 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
.title-text {
|
|
font-size: 16px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.system-status {
|
|
font-size: 10px;
|
|
color: #0a0;
|
|
}
|
|
|
|
.game-area {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 20px;
|
|
}
|
|
|
|
.stats-panel,
|
|
.command-panel,
|
|
.console-panel {
|
|
border: 2px solid #0f0;
|
|
padding: 15px;
|
|
}
|
|
|
|
.panel-title {
|
|
color: #0ff;
|
|
margin-bottom: 10px;
|
|
padding-bottom: 5px;
|
|
border-bottom: 1px solid #0f0;
|
|
}
|
|
|
|
.stat-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.stat-name {
|
|
color: #0a0;
|
|
min-width: 80px;
|
|
}
|
|
|
|
.stat-value {
|
|
color: #fff;
|
|
}
|
|
|
|
.stat-bar-text {
|
|
color: #0ff;
|
|
font-size: 10px;
|
|
}
|
|
|
|
.divider {
|
|
color: #050;
|
|
margin: 10px 0;
|
|
font-size: 8px;
|
|
}
|
|
|
|
.status-flag {
|
|
color: #ff0;
|
|
margin-top: 5px;
|
|
animation: blink 1s infinite;
|
|
}
|
|
|
|
.status-flag.sick {
|
|
color: #f00;
|
|
}
|
|
|
|
@keyframes blink {
|
|
0%, 50% { opacity: 1; }
|
|
51%, 100% { opacity: 0.3; }
|
|
}
|
|
|
|
.command-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 10px;
|
|
}
|
|
|
|
.cmd-btn {
|
|
background: #000;
|
|
border: 2px solid #0f0;
|
|
color: #0f0;
|
|
padding: 10px;
|
|
cursor: pointer;
|
|
font-family: 'Press Start 2P', monospace;
|
|
font-size: 10px;
|
|
transition: all 0.1s;
|
|
}
|
|
|
|
.cmd-btn:hover:not(:disabled) {
|
|
background: #0f0;
|
|
color: #000;
|
|
}
|
|
|
|
.cmd-btn:active:not(:disabled) {
|
|
transform: translate(2px, 2px);
|
|
}
|
|
|
|
.cmd-btn:disabled {
|
|
border-color: #030;
|
|
color: #030;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.console-panel {
|
|
grid-column: 1 / -1;
|
|
}
|
|
|
|
.console-log {
|
|
height: 150px;
|
|
overflow-y: auto;
|
|
background: #001100;
|
|
padding: 10px;
|
|
font-size: 10px;
|
|
}
|
|
|
|
.log-line {
|
|
margin-bottom: 5px;
|
|
color: #0c0;
|
|
}
|
|
|
|
.debug-toggle {
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
background: #000;
|
|
border: 2px solid #f0f;
|
|
color: #f0f;
|
|
padding: 5px 10px;
|
|
cursor: pointer;
|
|
font-size: 10px;
|
|
}
|
|
|
|
.debug-panel {
|
|
position: fixed;
|
|
top: 60px;
|
|
right: 20px;
|
|
background: #000;
|
|
border: 2px solid #f0f;
|
|
padding: 15px;
|
|
max-width: 300px;
|
|
}
|
|
|
|
.debug-panel .cmd-btn {
|
|
width: 100%;
|
|
margin-bottom: 5px;
|
|
border-color: #f0f;
|
|
color: #f0f;
|
|
}
|
|
|
|
.name-modal {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0, 0, 0, 0.9);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.modal-box {
|
|
background: #000;
|
|
border: 3px solid #0ff;
|
|
padding: 30px;
|
|
min-width: 400px;
|
|
}
|
|
|
|
.name-input {
|
|
width: 100%;
|
|
background: #001100;
|
|
border: 2px solid #0f0;
|
|
color: #0f0;
|
|
padding: 10px;
|
|
font-family: 'Press Start 2P', monospace;
|
|
font-size: 12px;
|
|
margin: 20px 0;
|
|
}
|
|
|
|
.name-input:focus {
|
|
outline: none;
|
|
border-color: #0ff;
|
|
}
|
|
|
|
/* 滚动条样式 */
|
|
::-webkit-scrollbar {
|
|
width: 10px;
|
|
}
|
|
|
|
::-webkit-scrollbar-track {
|
|
background: #000;
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb {
|
|
background: #0f0;
|
|
border: 1px solid #000;
|
|
}
|
|
</style>
|