Compare commits
17 Commits
6e3b86df24
...
main
Author | SHA1 | Date | |
---|---|---|---|
0be511c245 | |||
2487076e92 | |||
1f2a71452b | |||
e5af3b8c51 | |||
d1a08d055a | |||
64ca3bedf4 | |||
53b9694b09 | |||
9fbff6d8f2 | |||
a12cfc5a2a | |||
62634a426e | |||
88a9c6507c | |||
c2e5bf92a3 | |||
78c0f52c39 | |||
243c50a1a0 | |||
a624ba1e73 | |||
dd32327383 | |||
2202bd606f |
7
.env.example
Normal file
7
.env.example
Normal file
@@ -0,0 +1,7 @@
|
||||
VITE_FIREBASE_API_KEY=your-api-key
|
||||
VITE_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
|
||||
VITE_FIREBASE_PROJECT_ID=your-project-id
|
||||
VITE_FIREBASE_STORAGE_BUCKET=your-project.appspot.com
|
||||
VITE_FIREBASE_MESSAGING_SENDER_ID=your-sender-id
|
||||
VITE_FIREBASE_APP_ID=your-app-id
|
||||
VITE_FIREBASE_MEASUREMENT_ID=your-measurement-id
|
7
.env.production
Normal file
7
.env.production
Normal file
@@ -0,0 +1,7 @@
|
||||
VITE_FIREBASE_API_KEY=VITE_FIREBASE_API_KEY
|
||||
VITE_FIREBASE_AUTH_DOMAIN=VITE_FIREBASE_AUTH_DOMAIN
|
||||
VITE_FIREBASE_PROJECT_ID=VITE_FIREBASE_PROJECT_ID
|
||||
VITE_FIREBASE_STORAGE_BUCKET=VITE_FIREBASE_STORAGE_BUCKET
|
||||
VITE_FIREBASE_MESSAGING_SENDER_ID=VITE_FIREBASE_MESSAGING_SENDER_ID
|
||||
VITE_FIREBASE_APP_ID=VITE_FIREBASE_APP_ID
|
||||
VITE_FIREBASE_MEASUREMENT_ID=VITE_FIREBASE_MEASUREMENT_ID
|
58
.gitea/workflows/main.yml
Normal file
58
.gitea/workflows/main.yml
Normal file
@@ -0,0 +1,58 @@
|
||||
name: Main Build & Deploy
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and Push Docker Image (amd64 and arm64)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
config-inline: |
|
||||
[registry."git.ivanch.me"]
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: https://github.com/docker/login-action@v3.3.0
|
||||
with:
|
||||
registry: git.ivanch.me
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
|
||||
|
||||
- name: Build Docker image and push
|
||||
id: docker_build
|
||||
uses: https://github.com/docker/build-push-action@v6.12.0
|
||||
with:
|
||||
context: ./
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: git.ivanch.me/ivanch/pet-companion/pet-companion:latest
|
||||
platforms: linux/amd64, linux/arm64
|
||||
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
|
||||
deploy_live:
|
||||
name: Deploy Live
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
|
||||
steps:
|
||||
- name: Recreate container
|
||||
uses: https://github.com/appleboy/ssh-action@v1.2.0
|
||||
with:
|
||||
host: ${{ secrets.LIVE_HOST }}
|
||||
username: ${{ secrets.LIVE_USERNAME }}
|
||||
key: ${{ secrets.LIVE_KEY }}
|
||||
port: ${{ secrets.LIVE_PORT }}
|
||||
script: |
|
||||
cd ${{ secrets.LIVE_PROJECT_DIR }}
|
||||
docker compose down
|
||||
docker compose rm
|
||||
docker compose pull
|
||||
docker compose up -d
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -22,3 +22,5 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
.env
|
||||
|
18
Dockerfile
Normal file
18
Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM nginx:alpine
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY env.sh /docker-entrypoint.d/env.sh
|
||||
RUN chmod +x /docker-entrypoint.d/env.sh
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
12
env.sh
Normal file
12
env.sh
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
for i in $(env | grep VITE_)
|
||||
do
|
||||
key=$(echo $i | cut -d '=' -f 1)
|
||||
value=$(echo $i | cut -d '=' -f 2-)
|
||||
echo $key=$value
|
||||
# sed All files
|
||||
# find /usr/share/nginx/html -type f -exec sed -i "s|${key}|${value}|g" '{}' +
|
||||
|
||||
# sed JS and CSS only
|
||||
find /usr/share/nginx/html -type f \( -name '*.js' -o -name '*.css' \) -exec sed -i "s|${key}|${value}|g" '{}' +
|
||||
done
|
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
<title>Idle Pet</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
10
nginx.conf
Normal file
10
nginx.conf
Normal file
@@ -0,0 +1,10 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
1071
package-lock.json
generated
1071
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.7",
|
||||
"firebase": "^11.3.1",
|
||||
"lucide-react": "^0.344.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
|
57
src/App.tsx
57
src/App.tsx
@@ -1,14 +1,30 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { onAuthStateChanged } from 'firebase/auth';
|
||||
import { auth } from './config/firebase';
|
||||
import PetDisplay from './components/PetDisplay';
|
||||
import InteractionMenu from './components/InteractionMenu';
|
||||
import PetRegister from './components/PetRegister';
|
||||
import AnimatedBackground from './components/AnimatedBackground';
|
||||
import AuthForm from './components/auth/AuthForm';
|
||||
import { Pet } from './types/Pet';
|
||||
import { fetchPets } from './services/api/api';
|
||||
|
||||
export default function App() {
|
||||
const [pet, setPet] = useState<Pet | null>(null);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = onAuthStateChanged(auth, (user) => {
|
||||
setIsAuthenticated(!!user);
|
||||
setIsLoading(false);
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
const loadPets = async () => {
|
||||
try {
|
||||
const pets = await fetchPets();
|
||||
@@ -21,7 +37,8 @@ export default function App() {
|
||||
};
|
||||
|
||||
loadPets();
|
||||
}, []);
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
const handleCustomize = () => {
|
||||
console.log('Customize pet');
|
||||
@@ -31,14 +48,45 @@ export default function App() {
|
||||
setPet(updatedPet);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 flex items-center justify-center">
|
||||
<div className="text-white">Loading...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<>
|
||||
<AnimatedBackground />
|
||||
<div className="relative z-10">
|
||||
<AuthForm onAuthSuccess={() => setIsAuthenticated(true)} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (!pet) {
|
||||
return <PetRegister onPetCreated={setPet} />;
|
||||
return (
|
||||
<>
|
||||
<AnimatedBackground />
|
||||
<div className="relative z-10">
|
||||
<PetRegister onPetCreated={setPet} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white p-4">
|
||||
<>
|
||||
<AnimatedBackground />
|
||||
<div className="relative z-10 min-h-screen backdrop-blur-sm bg-gray-900/20 text-white p-4">
|
||||
<div className="max-w-4xl mx-auto grid gap-8">
|
||||
<PetDisplay pet={pet} />
|
||||
<PetDisplay
|
||||
pet={pet}
|
||||
onPetUpdate={handlePetUpdate} // Add this prop
|
||||
/>
|
||||
<InteractionMenu
|
||||
pet={pet}
|
||||
onPetUpdate={handlePetUpdate}
|
||||
@@ -46,5 +94,6 @@ export default function App() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
import { LucideIcon } from 'lucide-react';
|
||||
import { Pet, Resources } from '../types/Pet';
|
||||
import { Pet } from '../types/Pet';
|
||||
import Tooltip from './Tooltip';
|
||||
import { isActionActive, formatResourceName, getResourceFromAction } from '../utils/petUtils';
|
||||
|
||||
const colorClassMap = {
|
||||
@@ -11,6 +12,15 @@ const colorClassMap = {
|
||||
purple: 'bg-purple-900/30 hover:bg-purple-800/50 border-purple-500/50',
|
||||
} as const;
|
||||
|
||||
const activeColorClassMap = {
|
||||
amber: 'bg-amber-700/50 border-amber-400',
|
||||
emerald: 'bg-emerald-700/50 border-emerald-400',
|
||||
red: 'bg-red-700/50 border-red-400',
|
||||
green: 'bg-green-700/50 border-green-400',
|
||||
blue: 'bg-blue-700/50 border-blue-400',
|
||||
purple: 'bg-purple-700/50 border-purple-400',
|
||||
} as const;
|
||||
|
||||
const getActionVerb = (actionType: 'gather' | 'explore' | 'battle'): string => {
|
||||
const verbs = {
|
||||
gather: 'Gathering',
|
||||
@@ -20,6 +30,15 @@ const getActionVerb = (actionType: 'gather' | 'explore' | 'battle'): string => {
|
||||
return verbs[actionType];
|
||||
};
|
||||
|
||||
const getActionDescription = (actionType: 'gather' | 'explore' | 'battle'): string => {
|
||||
const descriptions = {
|
||||
gather: 'Send your pet to gather resources<br/>Your pet will collect items over time',
|
||||
explore: 'Send your pet to explore<br/>Your pet might find rare items',
|
||||
battle: 'Send your pet to battle<br/>Your pet will gain experience and items',
|
||||
};
|
||||
return descriptions[actionType];
|
||||
};
|
||||
|
||||
type ButtonColor = keyof typeof colorClassMap;
|
||||
|
||||
interface ActionResourceButtonProps {
|
||||
@@ -28,9 +47,9 @@ interface ActionResourceButtonProps {
|
||||
label: string;
|
||||
actionType: 'gather' | 'explore' | 'battle';
|
||||
color: ButtonColor;
|
||||
actionText?: string;
|
||||
onActionClick: () => void;
|
||||
onActionComplete: (updatedPet: Pet) => void;
|
||||
onResourcesUpdate: (resources: Resources) => void;
|
||||
}
|
||||
|
||||
export default function ActionResourceButton({
|
||||
@@ -39,25 +58,43 @@ export default function ActionResourceButton({
|
||||
label,
|
||||
actionType,
|
||||
color,
|
||||
actionText,
|
||||
onActionClick
|
||||
}: ActionResourceButtonProps) {
|
||||
const isActive = isActionActive(pet.petGatherAction, actionType);
|
||||
const currentResource = getResourceFromAction(pet.petGatherAction);
|
||||
|
||||
return (
|
||||
const getButtonText = () => {
|
||||
if (!isActive) return label;
|
||||
|
||||
if (actionType === 'explore' && pet.petGatherAction === 'EXPLORE') {
|
||||
return 'Exploring';
|
||||
}
|
||||
if (actionType === 'battle' && pet.petGatherAction === 'BATTLE') {
|
||||
return 'Battling';
|
||||
}
|
||||
if (currentResource) {
|
||||
return `${getActionVerb(actionType)} ${formatResourceName(currentResource)}`;
|
||||
}
|
||||
return label;
|
||||
};
|
||||
|
||||
const button = (
|
||||
<button
|
||||
onClick={onActionClick}
|
||||
className={`flex items-center justify-center space-x-2
|
||||
${colorClassMap[color]}
|
||||
${isActive ? activeColorClassMap[color] : colorClassMap[color]}
|
||||
border-2 rounded-lg p-4
|
||||
transition-all duration-300 transform hover:scale-105 w-full`}
|
||||
>
|
||||
<Icon className="w-6 h-6" />
|
||||
<span>
|
||||
{isActive && currentResource
|
||||
? `${getActionVerb(actionType)} ${formatResourceName(currentResource)}`
|
||||
: label}
|
||||
</span>
|
||||
<span>{getButtonText()}</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip content={actionText || ''}>
|
||||
{button}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
105
src/components/AnimatedBackground.tsx
Normal file
105
src/components/AnimatedBackground.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export default function AnimatedBackground() {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
|
||||
const particles: Particle[] = [];
|
||||
const particleCount = 50;
|
||||
|
||||
class Particle {
|
||||
x: number;
|
||||
y: number;
|
||||
size: number;
|
||||
speedX: number;
|
||||
speedY: number;
|
||||
opacity: number;
|
||||
|
||||
constructor() {
|
||||
this.x = Math.random() * canvas.width;
|
||||
this.y = Math.random() * canvas.height;
|
||||
this.size = Math.random() * 3 + 1;
|
||||
this.speedX = Math.random() * 1 - 0.5;
|
||||
this.speedY = Math.random() * 1 - 0.5;
|
||||
this.opacity = Math.random() * 0.5 + 0.2;
|
||||
}
|
||||
|
||||
update() {
|
||||
this.x += this.speedX;
|
||||
this.y += this.speedY;
|
||||
|
||||
if (this.x > canvas.width) this.x = 0;
|
||||
if (this.x < 0) this.x = canvas.width;
|
||||
if (this.y > canvas.height) this.y = 0;
|
||||
if (this.y < 0) this.y = canvas.height;
|
||||
}
|
||||
|
||||
draw() {
|
||||
if (!ctx) return;
|
||||
ctx.fillStyle = `rgba(255, 255, 255, ${this.opacity})`;
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
// Create particles
|
||||
for (let i = 0; i < particleCount; i++) {
|
||||
particles.push(new Particle());
|
||||
}
|
||||
|
||||
function connectParticles() {
|
||||
for (let i = 0; i < particles.length; i++) {
|
||||
for (let j = i + 1; j < particles.length; j++) {
|
||||
const dx = particles[i].x - particles[j].x;
|
||||
const dy = particles[i].y - particles[j].y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < 100) {
|
||||
if (!ctx) return;
|
||||
ctx.strokeStyle = `rgba(255, 255, 255, ${0.2 * (1 - distance/100)})`;
|
||||
ctx.lineWidth = 0.5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(particles[i].x, particles[i].y);
|
||||
ctx.lineTo(particles[j].x, particles[j].y);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function animate() {
|
||||
if (!ctx) return;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
particles.forEach(particle => {
|
||||
particle.update();
|
||||
particle.draw();
|
||||
});
|
||||
|
||||
connectParticles();
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
animate();
|
||||
|
||||
const handleResize = () => {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
return <canvas ref={canvasRef} className="fixed top-0 left-0 w-full h-full -z-10" />;
|
||||
}
|
@@ -1,20 +1,23 @@
|
||||
import { Resources } from '../types/Pet';
|
||||
import { putPetCollectResources } from '../services/api/api';
|
||||
import { PetActionGathered } from '../types/PetAction';
|
||||
import { Pet } from '../types/Pet';
|
||||
|
||||
interface CollectResourcesButtonProps {
|
||||
petId: string;
|
||||
resources: Resources;
|
||||
resources: PetActionGathered[];
|
||||
onCollect: () => void;
|
||||
onPetUpdate: (pet: Pet) => void;
|
||||
}
|
||||
|
||||
export default function CollectResourcesButton({ petId, resources, onCollect }: CollectResourcesButtonProps) {
|
||||
const hasResources = Object.values(resources).some(value => value > 0);
|
||||
export default function CollectResourcesButton({ petId, resources, onCollect, onPetUpdate }: CollectResourcesButtonProps) {
|
||||
const hasResources = Object.values(resources).length > 0;
|
||||
|
||||
if (!hasResources) return null;
|
||||
|
||||
const handleCollect = async () => {
|
||||
try {
|
||||
await putPetCollectResources(petId);
|
||||
const updatedPet = await putPetCollectResources(petId);
|
||||
onPetUpdate(updatedPet);
|
||||
onCollect();
|
||||
} catch (error) {
|
||||
console.error('Failed to collect resources:', error);
|
||||
@@ -24,17 +27,20 @@ export default function CollectResourcesButton({ petId, resources, onCollect }:
|
||||
return (
|
||||
<button
|
||||
onClick={handleCollect}
|
||||
className="flex items-center justify-center space-x-2
|
||||
className="flex flex-col items-center justify-center
|
||||
bg-green-900/30 hover:bg-green-800/50
|
||||
border-2 border-green-500/50 rounded-lg p-4
|
||||
transition-all duration-300 transform hover:scale-105 w-full mt-2"
|
||||
>
|
||||
<span>
|
||||
Collect: {Object.entries(resources)
|
||||
.filter(([_, value]) => value > 0)
|
||||
.map(([key, value]) => `${value} ${key}`)
|
||||
.join(', ')}
|
||||
<span className="font-bold mb-1">Collect:</span>
|
||||
{resources.map((item, index) => (
|
||||
<span key={index}>
|
||||
{item.gameItem
|
||||
? item.gameItem.name
|
||||
: `${item.resource} x${item.amount}`
|
||||
}
|
||||
</span>
|
||||
))}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
@@ -1,13 +1,12 @@
|
||||
import { Pizza, PlayCircle, Moon, Compass, Sword, FeatherIcon } from 'lucide-react';
|
||||
import CollectResourcesButton from './CollectResourcesButton';
|
||||
import { Pet, Resources } from '../types/Pet';
|
||||
import { Pet } from '../types/Pet';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { updatePetAction, getPetGatheredResources } from '../services/api/api';
|
||||
import { PetBasicAction } from '../types/PetUpdateActionRequest';
|
||||
import { PetActionGathered, PetBasicAction, PetGatherAction } from '../types/PetAction';
|
||||
import ActionButton from './button/ActionButton';
|
||||
import ActionResourceButton from './ActionResourceButton';
|
||||
import ResourceSelectionModal from './modal/ResourceSelectionModal';
|
||||
import { PetAction } from '../types/PetUpdateActionRequest';
|
||||
|
||||
interface InteractionMenuProps {
|
||||
pet: Pet;
|
||||
@@ -16,10 +15,9 @@ interface InteractionMenuProps {
|
||||
}
|
||||
|
||||
export default function InteractionMenu({ pet, onPetUpdate }: InteractionMenuProps) {
|
||||
const [gatheredResources, setGatheredResources] = useState<Resources>({ wisdom: 0, gold: 0, food: 0, junk: 0 });
|
||||
const [gatheredResources, setGatheredResources] = useState<PetActionGathered[]>([]);
|
||||
const [remainingCooldown, setRemainingCooldown] = useState<number | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [selectedActionType, setSelectedActionType] = useState<'gather' | 'explore' | 'battle' | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const updateCooldown = () => {
|
||||
@@ -78,14 +76,6 @@ export default function InteractionMenu({ pet, onPetUpdate }: InteractionMenuPro
|
||||
onPetUpdate(updatedPet);
|
||||
};
|
||||
|
||||
const handleResourcesUpdate = (resources: Resources) => {
|
||||
setGatheredResources(resources);
|
||||
};
|
||||
|
||||
const handleCollect = () => {
|
||||
setGatheredResources({ wisdom: 0, gold: 0, food: 0, junk: 0 });
|
||||
};
|
||||
|
||||
function performBasicAction(basicAction: PetBasicAction): () => void {
|
||||
return async () => {
|
||||
try {
|
||||
@@ -99,13 +89,12 @@ export default function InteractionMenu({ pet, onPetUpdate }: InteractionMenuPro
|
||||
|
||||
const handleActionStart = async (actionType: 'gather' | 'explore' | 'battle') => {
|
||||
if (actionType === 'gather') {
|
||||
setSelectedActionType(actionType);
|
||||
setIsModalOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const action: PetAction = actionType === 'explore' ? 'EXPLORING' : 'BATTLE';
|
||||
const action: PetGatherAction = actionType === 'explore' ? 'EXPLORE' : 'BATTLE';
|
||||
const updatedPet = await updatePetAction(pet.id, { gatherAction: action });
|
||||
onPetUpdate(updatedPet);
|
||||
} catch (error) {
|
||||
@@ -126,7 +115,7 @@ export default function InteractionMenu({ pet, onPetUpdate }: InteractionMenuPro
|
||||
}
|
||||
|
||||
try {
|
||||
const action: PetAction = `GATHERING_${resourceType.toUpperCase()}` as PetAction;
|
||||
const action: PetGatherAction = `GATHERING_${resourceType.toUpperCase()}` as PetGatherAction;
|
||||
const updatedPet = await updatePetAction(pet.id, { gatherAction: action });
|
||||
onPetUpdate(updatedPet);
|
||||
} catch (error) {
|
||||
@@ -136,8 +125,12 @@ export default function InteractionMenu({ pet, onPetUpdate }: InteractionMenuPro
|
||||
}
|
||||
};
|
||||
|
||||
const handleCollect = () => {
|
||||
setGatheredResources([]);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 bg-gray-900 p-6 rounded-xl shadow-xl">
|
||||
{remainingCooldown !== null && (
|
||||
<div className="col-span-2 md:col-span-3 text-center p-2 bg-yellow-900/30 border-2 border-yellow-500/50 rounded-lg">
|
||||
<span className="text-yellow-200">Cooldown: {formatCooldownTime(remainingCooldown)}</span>
|
||||
@@ -150,6 +143,7 @@ export default function InteractionMenu({ pet, onPetUpdate }: InteractionMenuPro
|
||||
onClick={performBasicAction('FEED')}
|
||||
color="green"
|
||||
disabled={remainingCooldown !== null}
|
||||
actionText="Feed your pet<br/>+1 Strengh (up to max)<br/>+5 Health<br/>-1 Food"
|
||||
/>
|
||||
<ActionButton
|
||||
icon={PlayCircle}
|
||||
@@ -157,6 +151,7 @@ export default function InteractionMenu({ pet, onPetUpdate }: InteractionMenuPro
|
||||
onClick={performBasicAction('PLAY')}
|
||||
color="blue"
|
||||
disabled={remainingCooldown !== null}
|
||||
actionText="Play with your pet<br/>+1 Charisma (up to max)<br/>-1 Junk"
|
||||
/>
|
||||
<ActionButton
|
||||
icon={Moon}
|
||||
@@ -164,6 +159,7 @@ export default function InteractionMenu({ pet, onPetUpdate }: InteractionMenuPro
|
||||
onClick={performBasicAction('SLEEP')}
|
||||
color="purple"
|
||||
disabled={remainingCooldown !== null}
|
||||
actionText="Put your pet to sleep<br/>+1 Intelligence (up to max)<br/>+1 Strengh (up to max)<br/>+15 Health"
|
||||
/>
|
||||
|
||||
<div className="col-span-2 md:col-span-3 grid grid-cols-3 gap-4">
|
||||
@@ -175,7 +171,7 @@ export default function InteractionMenu({ pet, onPetUpdate }: InteractionMenuPro
|
||||
color="amber"
|
||||
onActionClick={() => handleActionStart('gather')}
|
||||
onActionComplete={handleGatherComplete}
|
||||
onResourcesUpdate={handleResourcesUpdate}
|
||||
actionText='Send your pet to gather resources<br/>Your pet will collect items over time'
|
||||
/>
|
||||
<ActionResourceButton
|
||||
pet={pet}
|
||||
@@ -185,7 +181,7 @@ export default function InteractionMenu({ pet, onPetUpdate }: InteractionMenuPro
|
||||
color="emerald"
|
||||
onActionClick={() => handleActionStart('explore')}
|
||||
onActionComplete={handleGatherComplete}
|
||||
onResourcesUpdate={handleResourcesUpdate}
|
||||
actionText='Send your pet to explore based on their strength<br/>Your pet might find wisdom and rare items but also may lose health'
|
||||
/>
|
||||
<ActionResourceButton
|
||||
pet={pet}
|
||||
@@ -195,7 +191,7 @@ export default function InteractionMenu({ pet, onPetUpdate }: InteractionMenuPro
|
||||
color="red"
|
||||
onActionClick={() => handleActionStart('battle')}
|
||||
onActionComplete={handleGatherComplete}
|
||||
onResourcesUpdate={handleResourcesUpdate}
|
||||
actionText='Send your pet to battle based on their strength<br/>Your pet will gain experience and items, but also may lose health'
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -204,6 +200,7 @@ export default function InteractionMenu({ pet, onPetUpdate }: InteractionMenuPro
|
||||
petId={pet.id}
|
||||
resources={gatheredResources}
|
||||
onCollect={handleCollect}
|
||||
onPetUpdate={onPetUpdate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@@ -1,18 +1,23 @@
|
||||
import { Pet } from '../types/Pet';
|
||||
import { Brain, Dumbbell, Heart, Sparkles, Coins, Pizza, Trash2, Trophy } from 'lucide-react';
|
||||
import { Brain, Dumbbell, Smile, Sparkles, Coins, Pizza, Trash2, Trophy, Heart, Star } from 'lucide-react';
|
||||
import { PET_CLASSES } from '../data/petClasses';
|
||||
import { useState } from 'react';
|
||||
import InventoryModal from './modal/InventoryModal';
|
||||
import SkillTreeModal from './modal/SkillTreeModal';
|
||||
|
||||
interface PetDisplayProps {
|
||||
pet: Pet;
|
||||
onPetUpdate: (updatedPet: Pet) => void; // Add this prop
|
||||
}
|
||||
|
||||
export default function PetDisplay({ pet }: PetDisplayProps) {
|
||||
const StatBar = ({ value, maxValue, label, icon: Icon }: { value: number; maxValue: number; label: string; icon: any }) => (
|
||||
<div className="flex items-center space-x-2">
|
||||
export default function PetDisplay({ pet, onPetUpdate }: PetDisplayProps) {
|
||||
const StatBar = ({ value, maxValue, label, icon: Icon }:
|
||||
{ value: number; maxValue: number; label: string; icon: any; }) => (
|
||||
<div className="flex items-center space-x-2" title={label}>
|
||||
<Icon className="w-5 h-5" />
|
||||
<div className="flex-1 bg-gray-700 rounded-full h-4">
|
||||
<div
|
||||
className="bg-blue-500 rounded-full h-4"
|
||||
className={`bg-blue-500 rounded-full h-4`}
|
||||
style={{ width: `${(value / maxValue) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
@@ -28,6 +33,9 @@ export default function PetDisplay({ pet }: PetDisplayProps) {
|
||||
</div>
|
||||
);
|
||||
|
||||
const [showInventoryModal, setShowInventoryModal] = useState(false);
|
||||
const [showSkillTreeModal, setShowSkillTreeModal] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="bg-gray-900 p-6 rounded-xl shadow-xl">
|
||||
<div className="text-center mb-8">
|
||||
@@ -47,6 +55,14 @@ export default function PetDisplay({ pet }: PetDisplayProps) {
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 mb-6">
|
||||
<div className="space-y-4 mb-6">
|
||||
<StatBar
|
||||
value={pet.health}
|
||||
maxValue={pet.maxHealth}
|
||||
label="Health"
|
||||
icon={Heart}
|
||||
/>
|
||||
</div>
|
||||
<StatBar
|
||||
value={pet.stats.intelligence}
|
||||
maxValue={pet.stats.maxIntelligence}
|
||||
@@ -63,16 +79,50 @@ export default function PetDisplay({ pet }: PetDisplayProps) {
|
||||
value={pet.stats.charisma}
|
||||
maxValue={pet.stats.maxCharisma}
|
||||
label="Charisma"
|
||||
icon={Heart}
|
||||
icon={Smile}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex items-start mb-4 space-x-4">
|
||||
<div className="grid grid-cols-2 gap-4 flex-1">
|
||||
<ResourceCounter value={pet.resources.wisdom} label="Wisdom" icon={Sparkles} />
|
||||
<ResourceCounter value={pet.resources.gold} label="Gold" icon={Coins} />
|
||||
<ResourceCounter value={pet.resources.food} label="Food" icon={Pizza} />
|
||||
<ResourceCounter value={pet.resources.junk} label="Junk" icon={Trash2} />
|
||||
</div>
|
||||
<div className="flex flex-col space-y-4">
|
||||
<button
|
||||
onClick={() => setShowInventoryModal(true)}
|
||||
className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded"
|
||||
>
|
||||
Inventory
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowSkillTreeModal(true)}
|
||||
className="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded"
|
||||
>
|
||||
Skill Tree
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showInventoryModal && (
|
||||
<InventoryModal
|
||||
inventory={pet.inventory}
|
||||
petId={pet.id}
|
||||
onClose={() => setShowInventoryModal(false)}
|
||||
onPetUpdate={onPetUpdate}
|
||||
/>
|
||||
)}
|
||||
{showSkillTreeModal && (
|
||||
<SkillTreeModal
|
||||
petId={pet.id}
|
||||
petResources={pet.resources}
|
||||
onPetUpdate={onPetUpdate}
|
||||
onClose={() => setShowSkillTreeModal(false)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
20
src/components/Tooltip.tsx
Normal file
20
src/components/Tooltip.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
interface TooltipProps {
|
||||
content: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function Tooltip({ content, children }: TooltipProps) {
|
||||
return (
|
||||
<div className="group relative inline-block w-full">
|
||||
{children}
|
||||
<div className="opacity-0 group-hover:opacity-100 transition-opacity duration-300
|
||||
absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-2
|
||||
bg-gray-900 text-white text-sm rounded-lg shadow-lg whitespace-nowrap
|
||||
border border-gray-700 pointer-events-none z-50">
|
||||
<div dangerouslySetInnerHTML={{ __html: content }} />
|
||||
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2
|
||||
border-4 border-transparent border-t-gray-900" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
129
src/components/auth/AuthForm.tsx
Normal file
129
src/components/auth/AuthForm.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import { useState } from 'react';
|
||||
import { login, register, signInWithGoogle } from '../../services/auth/auth';
|
||||
|
||||
interface AuthFormProps {
|
||||
onAuthSuccess: () => void;
|
||||
}
|
||||
|
||||
export default function AuthForm({ onAuthSuccess }: AuthFormProps) {
|
||||
const [isLogin, setIsLogin] = useState(true);
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
|
||||
try {
|
||||
if (isLogin) {
|
||||
await login(email, password);
|
||||
} else {
|
||||
await register(email, password);
|
||||
}
|
||||
onAuthSuccess();
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoogleSignIn = async () => {
|
||||
try {
|
||||
await signInWithGoogle();
|
||||
onAuthSuccess();
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-900">
|
||||
<div className="bg-gray-800 p-8 rounded-xl shadow-xl w-96">
|
||||
<h2 className="text-2xl font-bold text-white mb-6 text-center">
|
||||
{isLogin ? 'Login' : 'Register'}
|
||||
</h2>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-500 text-white p-3 rounded mb-4 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full p-3 rounded bg-gray-700 text-white"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full p-3 rounded bg-gray-700 text-white"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white p-3 rounded"
|
||||
>
|
||||
{isLogin ? 'Login' : 'Register'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-4 relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-gray-600"></div>
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-2 bg-gray-800 text-gray-400">Or continue with</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGoogleSignIn}
|
||||
className="mt-4 w-full flex items-center justify-center gap-2 bg-white hover:bg-gray-100 text-gray-900 font-semibold p-3 rounded"
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||
fill="#4285F4"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||
fill="#34A853"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||
fill="#FBBC05"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||
fill="#EA4335"
|
||||
/>
|
||||
</svg>
|
||||
Continue with Google
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setIsLogin(!isLogin)}
|
||||
className="w-full text-blue-400 mt-4 text-sm"
|
||||
>
|
||||
{isLogin ? 'Need an account? Register' : 'Have an account? Login'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -1,4 +1,5 @@
|
||||
import { LucideIcon } from 'lucide-react';
|
||||
import Tooltip from '../Tooltip';
|
||||
|
||||
interface ActionButtonProps {
|
||||
icon: LucideIcon;
|
||||
@@ -6,10 +7,11 @@ interface ActionButtonProps {
|
||||
onClick: () => void;
|
||||
color: string;
|
||||
disabled?: boolean;
|
||||
actionText?: string;
|
||||
}
|
||||
|
||||
export default function ActionButton({ icon: Icon, label, onClick, color, disabled }: ActionButtonProps) {
|
||||
return (
|
||||
export default function ActionButton({ icon: Icon, label, onClick, color, disabled, actionText }: ActionButtonProps) {
|
||||
const button = (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
@@ -22,4 +24,14 @@ export default function ActionButton({ icon: Icon, label, onClick, color, disabl
|
||||
<span>{label}</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
if (!actionText) {
|
||||
return button;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip content={actionText}>
|
||||
{button}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
201
src/components/modal/InventoryModal.tsx
Normal file
201
src/components/modal/InventoryModal.tsx
Normal file
@@ -0,0 +1,201 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Inventory, InvItemInteraction, Pet } from '../../types/Pet';
|
||||
import { putPetItemInteract, getItemIcon, getItemInfo } from '../../services/api/api';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { GameItem, ItemRarity } from '../../types/GameItem';
|
||||
|
||||
interface InventoryModalProps {
|
||||
inventory: Inventory;
|
||||
petId: string;
|
||||
onClose: () => void;
|
||||
onPetUpdate: (updatedPet: Pet) => void;
|
||||
}
|
||||
|
||||
export default function InventoryModal({ inventory, petId, onClose, onPetUpdate }: InventoryModalProps) {
|
||||
const capacity = inventory.capacity;
|
||||
const items = inventory.items;
|
||||
const gridSlots = Array.from({ length: capacity }, (_, i) => items[i]);
|
||||
const [selectedItemIndex, setSelectedItemIndex] = useState<number | null>(null);
|
||||
const [itemIcons, setItemIcons] = useState<Map<number, string>>(new Map());
|
||||
const [loadingIcons, setLoadingIcons] = useState<Set<number>>(new Set());
|
||||
const [selectedItemDetails, setSelectedItemDetails] = useState<GameItem | null>(null);
|
||||
const [loadingDetails, setLoadingDetails] = useState(false);
|
||||
|
||||
const rarityColors = {
|
||||
[ItemRarity.Common]: 'text-gray-200',
|
||||
[ItemRarity.Uncommon]: 'text-green-400',
|
||||
[ItemRarity.Rare]: 'text-blue-400',
|
||||
[ItemRarity.Legendary]: 'text-purple-400'
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchItemIcons = async () => {
|
||||
const newIcons = new Map<number, string>();
|
||||
const loadingItems = new Set<number>();
|
||||
|
||||
for (const itemId of items) {
|
||||
if (itemId !== undefined && !itemIcons.has(itemId)) {
|
||||
loadingItems.add(itemId);
|
||||
}
|
||||
}
|
||||
setLoadingIcons(loadingItems);
|
||||
|
||||
for (const itemId of loadingItems) {
|
||||
if (itemId !== undefined && !itemIcons.has(itemId)) {
|
||||
try {
|
||||
const blob = await getItemIcon(itemId);
|
||||
const imageUrl = URL.createObjectURL(blob);
|
||||
newIcons.set(itemId, imageUrl);
|
||||
} catch (error) {
|
||||
console.error(`Failed to load icon for item ${itemId}:`, error);
|
||||
} finally {
|
||||
loadingItems.delete(itemId);
|
||||
setLoadingIcons(new Set(loadingItems));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setItemIcons(new Map([...itemIcons, ...newIcons]));
|
||||
};
|
||||
|
||||
fetchItemIcons();
|
||||
|
||||
return () => {
|
||||
// Cleanup object URLs on unmount
|
||||
itemIcons.forEach(url => URL.revokeObjectURL(url));
|
||||
};
|
||||
}, [items]);
|
||||
|
||||
const handleItemClick = async (index: number) => {
|
||||
if (selectedItemIndex === index || gridSlots[index] === undefined) {
|
||||
setSelectedItemIndex(null);
|
||||
setSelectedItemDetails(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedItemIndex(index);
|
||||
const itemId = gridSlots[index];
|
||||
|
||||
if (itemId !== undefined) {
|
||||
setLoadingDetails(true);
|
||||
try {
|
||||
const details = await getItemInfo(itemId);
|
||||
setSelectedItemDetails(details);
|
||||
} catch (error) {
|
||||
console.error('Failed to load item details:', error);
|
||||
} finally {
|
||||
setLoadingDetails(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleInteraction = async (interaction: InvItemInteraction) => {
|
||||
if (selectedItemIndex === null || gridSlots[selectedItemIndex] === undefined) return;
|
||||
try {
|
||||
const updatedPet = await putPetItemInteract(petId, gridSlots[selectedItemIndex], interaction);
|
||||
onPetUpdate(updatedPet);
|
||||
} catch (error) {
|
||||
console.error('Item interaction failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-gray-800 p-6 rounded-xl max-w-2xl w-full">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-2xl font-bold">Inventory</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="bg-red-600 hover:bg-red-700 text-white px-2 py-1 rounded"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-5 grid-rows-4 gap-4">
|
||||
{gridSlots.map((itemId, index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => handleItemClick(index)}
|
||||
className={`border rounded p-2 flex items-center justify-center h-24 w-24
|
||||
${itemId !== undefined ? 'bg-gray-700 cursor-pointer' : 'bg-gray-800 text-gray-500'}
|
||||
${selectedItemIndex === index ? 'border-blue-500 animate-pulse' : 'border-gray-600'}`}
|
||||
>
|
||||
{itemId !== undefined ? (
|
||||
loadingIcons.has(itemId) ? (
|
||||
<Loader2 className="w-6 h-6 animate-spin" />
|
||||
) : (
|
||||
<img
|
||||
src={itemIcons.get(itemId)}
|
||||
alt={`Item ${itemId}`}
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<span className="text-gray-500">Empty</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selectedItemIndex !== null && (
|
||||
<div className="mt-4 mb-4 p-4 bg-gray-700 rounded">
|
||||
{loadingDetails ? (
|
||||
<div className="flex justify-center">
|
||||
<Loader2 className="w-6 h-6 animate-spin" />
|
||||
</div>
|
||||
) : selectedItemDetails ? (
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-semibold text-white mb-1">
|
||||
{selectedItemDetails.name}
|
||||
</h3>
|
||||
<div className="flex gap-2 items-center">
|
||||
<span className={rarityColors[selectedItemDetails.rarity]}>
|
||||
{selectedItemDetails.rarity}
|
||||
</span>
|
||||
<span className="text-gray-300">•</span>
|
||||
<span className="text-gray-200">{selectedItemDetails.type}</span>
|
||||
</div>
|
||||
<div className="text-gray-300 text-sm space-y-1">
|
||||
{selectedItemDetails.description.split(';').map((line, index) => (
|
||||
<p key={index}>{line.trim()}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 grid grid-cols-4 gap-4">
|
||||
<button
|
||||
disabled={selectedItemIndex === null}
|
||||
onClick={() => handleInteraction('USE')}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded disabled:opacity-50"
|
||||
>
|
||||
Use
|
||||
</button>
|
||||
<button
|
||||
disabled={selectedItemIndex === null}
|
||||
onClick={() => handleInteraction('DROP')}
|
||||
className="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded disabled:opacity-50"
|
||||
>
|
||||
Drop
|
||||
</button>
|
||||
<button
|
||||
disabled={selectedItemIndex === null}
|
||||
onClick={() => handleInteraction('EQUIP')}
|
||||
className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded disabled:opacity-50"
|
||||
>
|
||||
Equip
|
||||
</button>
|
||||
<button
|
||||
disabled={selectedItemIndex === null}
|
||||
onClick={() => handleInteraction('UNEQUIP')}
|
||||
className="bg-yellow-600 hover:bg-yellow-700 text-white px-4 py-2 rounded disabled:opacity-50"
|
||||
>
|
||||
Unequip
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
22
src/components/modal/SkillTreeModal.module.css
Normal file
22
src/components/modal/SkillTreeModal.module.css
Normal file
@@ -0,0 +1,22 @@
|
||||
.customScroll {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #4B5563 #1F2937;
|
||||
}
|
||||
|
||||
.customScroll::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.customScroll::-webkit-scrollbar-track {
|
||||
background: #1F2937;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.customScroll::-webkit-scrollbar-thumb {
|
||||
background-color: #4B5563;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.customScroll::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #6B7280;
|
||||
}
|
213
src/components/modal/SkillTreeModal.tsx
Normal file
213
src/components/modal/SkillTreeModal.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Pet, Resources } from '../../types/Pet';
|
||||
import { Skill, PetSkill, SkillType } from '../../types/Skills';
|
||||
import { fetchPets, getAllSkills, getPetSkills, postAllocatePetSkill } from '../../services/api/api';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import styles from './SkillTreeModal.module.css';
|
||||
|
||||
interface SkillTreeModalProps {
|
||||
onClose: () => void;
|
||||
petId: string;
|
||||
petResources: Resources;
|
||||
onPetUpdate: (updatedPet: Pet) => void;
|
||||
}
|
||||
|
||||
export default function SkillTreeModal({ onClose, petId, petResources, onPetUpdate }: SkillTreeModalProps) {
|
||||
const [skills, setSkills] = useState<Skill[]>([]);
|
||||
const [petSkills, setPetSkills] = useState<PetSkill[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [allocating, setAllocating] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const [allSkills, ownedSkills] = await Promise.all([
|
||||
getAllSkills(),
|
||||
getPetSkills(petId)
|
||||
]);
|
||||
setSkills(allSkills);
|
||||
setPetSkills(ownedSkills);
|
||||
} catch (error) {
|
||||
console.error('Error fetching skills:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, [petId]);
|
||||
|
||||
const handleAllocateSkill = async (skillId: number) => {
|
||||
try {
|
||||
setAllocating(true);
|
||||
const updatedPetSkill = await postAllocatePetSkill(petId, skillId);
|
||||
setPetSkills([...petSkills, updatedPetSkill]);
|
||||
// Refresh pet data to update skill points
|
||||
const updatedPet = await fetchPets();
|
||||
onPetUpdate(updatedPet[0]);
|
||||
} catch (error) {
|
||||
console.error('Error allocating skill:', error);
|
||||
} finally {
|
||||
setAllocating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getSkillTier = (skillId: number) => {
|
||||
return petSkills.find(ps => ps.skillId === skillId)?.currentTier;
|
||||
};
|
||||
|
||||
const canAllocateSkill = (skill: Skill) => {
|
||||
// check if all required skills are owned, and has all the required resources
|
||||
const requiredSkills = skills.filter(s => skill.skillsIdRequired?.includes(s.id));
|
||||
const requiredSkillIds = requiredSkills.map(s => s.id);
|
||||
const ownedRequiredSkills = petSkills.filter(ps => requiredSkillIds.includes(ps.skillId));
|
||||
const hasAllRequiredSkills = requiredSkills.length === ownedRequiredSkills.length;
|
||||
|
||||
const hasAllResources = skill.skillRequirements.every(req => {
|
||||
const resourceValue = petResources[req.resource.toLowerCase() as keyof Resources];
|
||||
return resourceValue >= req.cost;
|
||||
});
|
||||
|
||||
return hasAllRequiredSkills && hasAllResources;
|
||||
};
|
||||
|
||||
const getRequiredSkillNames = (skillId: number[] | null) => {
|
||||
if (!skillId) return null;
|
||||
return skills.filter(s => skillId.includes(s.id)).map(s => s.name);
|
||||
};
|
||||
|
||||
const getNextTierEffect = (currentTier?: string) => {
|
||||
if (!currentTier) return 'I';
|
||||
if (currentTier === 'I') return 'II';
|
||||
if (currentTier === 'II') return 'III';
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderSkillNode = (skill: Skill) => {
|
||||
const owned = petSkills.some(ps => ps.skillId === skill.id);
|
||||
const tier = getSkillTier(skill.id);
|
||||
const isMaxTier = tier === 'III';
|
||||
const canAllocate = !isMaxTier && canAllocateSkill(skill);
|
||||
const nextTierEffect = getNextTierEffect(tier);
|
||||
const requiredSkillNames = getRequiredSkillNames(skill.skillsIdRequired);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={skill.id}
|
||||
className={`
|
||||
relative p-4 rounded-lg border-2 flex flex-col
|
||||
${isMaxTier ? 'border-green-500 bg-gray-700' :
|
||||
owned ? 'border-blue-500 bg-gray-700' : 'border-gray-600 bg-gray-800'}
|
||||
transition-colors
|
||||
`}
|
||||
>
|
||||
<div className="flex-grow">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span>{skill.icon}</span>
|
||||
<h3 className="font-bold">{skill.name}</h3>
|
||||
{tier && (
|
||||
<span className={`px-2 py-1 rounded text-xs ${isMaxTier ? 'bg-green-600' : 'bg-blue-600'}`}>
|
||||
Tier {tier}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-300 mb-2">{skill.description}</p>
|
||||
|
||||
<div className="text-sm space-y-2 mb-3">
|
||||
{!isMaxTier && nextTierEffect && (
|
||||
<div className="text-blue-300">
|
||||
Next: Tier {nextTierEffect}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isMaxTier && nextTierEffect && (
|
||||
<div className="space-y-1">
|
||||
<div className="text-gray-400 font-medium">Requirements:</div>
|
||||
{requiredSkillNames && requiredSkillNames.map((name, idx) => (
|
||||
<div key={idx} className={`text-xs ${canAllocate ? 'text-green-400' : 'text-red-400'}`}>
|
||||
• Requires: {name}
|
||||
</div>
|
||||
))}
|
||||
{skill.skillRequirements.map((req, idx) => (
|
||||
<div key={idx} className={`text-xs ${petResources[req.resource.toLowerCase() as keyof Resources] >= req.cost
|
||||
? 'text-green-400' : 'text-red-400'
|
||||
}`}>
|
||||
• {req.resource}: {req.cost}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isMaxTier ? (
|
||||
<div className="mt-auto px-3 py-1 rounded text-sm bg-green-600 text-center">
|
||||
Mastered
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => handleAllocateSkill(skill.id)}
|
||||
disabled={!canAllocate || allocating}
|
||||
className={`
|
||||
mt-auto px-3 py-1 rounded text-sm
|
||||
${canAllocate
|
||||
? owned
|
||||
? 'bg-blue-600 hover:bg-blue-700'
|
||||
: 'bg-green-600 hover:bg-green-700'
|
||||
: 'bg-gray-600 cursor-not-allowed'}
|
||||
`}
|
||||
>
|
||||
{allocating ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
owned ? 'Upgrade' : 'Learn'
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSkillTree = () => {
|
||||
const groundSkills = skills.filter(s => s.type === 'GROUND');
|
||||
const grandSkills = skills.filter(s => s.type === 'GRAND');
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{groundSkills.map(renderSkillNode)}
|
||||
</div>
|
||||
<div className="border-t border-gray-600 pt-6">
|
||||
<h2 className="text-xl font-bold mb-4">Advanced Skills</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{grandSkills.map(renderSkillNode)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50">
|
||||
<div className={`bg-gray-800 p-6 rounded-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto ${styles.customScroll}`}>
|
||||
<h2 className="text-2xl font-bold mb-4">Skill Tree</h2>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center items-center h-40">
|
||||
<Loader2 className="w-8 h-8 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
renderSkillTree()
|
||||
)}
|
||||
|
||||
<div className="mt-6 flex justify-end">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
19
src/config/firebase.ts
Normal file
19
src/config/firebase.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { initializeApp } from 'firebase/app';
|
||||
import { getFirestore } from 'firebase/firestore';
|
||||
import { getAuth } from 'firebase/auth';
|
||||
import { getAnalytics } from 'firebase/analytics';
|
||||
|
||||
const firebaseConfig = {
|
||||
apiKey: import.meta.env.VITE_FIREBASE_API_KEY || 'VITE_FIREBASE_API_KEY',
|
||||
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN || 'VITE_FIREBASE_AUTH_DOMAIN',
|
||||
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID || 'VITE_FIREBASE_PROJECT_ID',
|
||||
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET || 'VITE_FIREBASE_STORAGE_BUCKET',
|
||||
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID || 'VITE_FIREBASE_MESSAGING_SENDER_ID',
|
||||
appId: import.meta.env.VITE_FIREBASE_APP_ID || 'VITE_FIREBASE_APP_ID',
|
||||
measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID || 'VITE_FIREBASE_MEASUREMENT_ID',
|
||||
};
|
||||
|
||||
const app = initializeApp(firebaseConfig);
|
||||
export const db = getFirestore(app);
|
||||
export const auth = getAuth(app);
|
||||
export const analytics = getAnalytics(app);
|
@@ -1,3 +1,18 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
}
|
||||
|
||||
.backdrop-blur-sm {
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
/* Add smooth transitions */
|
||||
.transition-all {
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
@@ -1,57 +1,69 @@
|
||||
import { ApiService } from './index';
|
||||
import { Pet, Resources } from '../../types/Pet';
|
||||
import { InvItemInteraction, Pet, Resources } from '../../types/Pet';
|
||||
import { PetCreationRequest } from '../../types/PetCreationRequest';
|
||||
import { PetUpdateActionRequest } from '../../types/PetUpdateActionRequest';
|
||||
import { PetActionGathered, PetUpdateActionRequest } from '../../types/PetAction';
|
||||
import { PetSkill, Skill } from '../../types/Skills';
|
||||
import { GameItem } from '../../types/GameItem';
|
||||
|
||||
// Get API service instance
|
||||
const api = ApiService.getInstance();
|
||||
|
||||
export async function fetchPets(): Promise<Pet[]> {
|
||||
try {
|
||||
const response = await api.get<Pet[]>('/api/v1/pet');
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
console.error('Failed to fetch pets:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createPet(data: PetCreationRequest): Promise<Pet> {
|
||||
try {
|
||||
const response = await api.post<Pet>('/api/v1/pet', data);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
console.error('Failed to create pet:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updatePetAction(petId: string, data: PetUpdateActionRequest): Promise<Pet> {
|
||||
try {
|
||||
const response = await api.put<Pet>(`/api/v1/pet/${petId}/action`, data);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
console.error('Failed to update pet action:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPetGatheredResources(petId: string): Promise<Resources> {
|
||||
try {
|
||||
const response = await api.get<Resources>(`/api/v1/pet/${petId}/resources/gathered`);
|
||||
export async function getPetGatheredResources(petId: string): Promise<PetActionGathered[]> {
|
||||
const response = await api.get<PetActionGathered[]>(`/api/v1/pet/${petId}/resources/gathered`);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
console.error('Failed to fetch pet gathered resources:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function putPetCollectResources(petId: string): Promise<Pet> {
|
||||
try {
|
||||
const response = await api.put<Pet>(`/api/v1/pet/${petId}/resources/collect`);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
console.error('Failed to collect pet gathered resources:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllSkills(): Promise<Skill[]> {
|
||||
const response = await api.get<Skill[]>(`/api/v1/skill`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getPetSkills(petId: string): Promise<PetSkill[]> {
|
||||
const response = await api.get<PetSkill[]>(`/api/v1/skill/${petId}/skills`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function postAllocatePetSkill(petId: string, skillId: number): Promise<PetSkill> {
|
||||
const response = await api.post<PetSkill>(`/api/v1/skill/${petId}/allocate/${skillId}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function putPetItemInteract(petId: string, itemId: number, inter: InvItemInteraction): Promise<Pet> {
|
||||
const response = await api.put<Pet>(`/api/v1/inventory/${petId}/${itemId}/${inter.toLowerCase()}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getItemInfo(itemId: number): Promise<GameItem> {
|
||||
const response = await api.get<GameItem>(`/api/v1/gamedata/item/${itemId}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getItemIcon(itemId: number): Promise<Blob> {
|
||||
const response = await api.get<Blob>(`/api/v1/gamedata/item/${itemId}/icon`, {
|
||||
responseType: 'blob',
|
||||
headers: {
|
||||
Accept: 'image/png'
|
||||
}
|
||||
});
|
||||
return response.data;
|
||||
}
|
@@ -2,7 +2,7 @@ import { ApiConfig } from './types';
|
||||
|
||||
// API configuration
|
||||
export const apiConfig: ApiConfig = {
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:5278',
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || 'VITE_API_BASE_URL',
|
||||
timeout: 10000, // 10 seconds
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
@@ -49,4 +49,8 @@ export class ApiErrorHandler {
|
||||
static isClientError(error: ApiError): boolean {
|
||||
return error.status >= 400 && error.status < 500;
|
||||
}
|
||||
|
||||
public static isUnauthorizedError(error: ApiError): boolean {
|
||||
return error.status === 401;
|
||||
}
|
||||
}
|
@@ -3,17 +3,60 @@ import { apiConfig } from './config';
|
||||
import { setupInterceptors } from './interceptors';
|
||||
import { ApiErrorHandler } from './error';
|
||||
import { ApiResponse, RequestOptions, ApiError } from './types';
|
||||
import { auth } from '../../config/firebase';
|
||||
|
||||
class ApiService {
|
||||
private static instance: ApiService;
|
||||
private axios: AxiosInstance;
|
||||
private axiosInstance: AxiosInstance;
|
||||
|
||||
private constructor() {
|
||||
// Create axios instance with base configuration
|
||||
this.axios = axios.create(apiConfig);
|
||||
this.axiosInstance = axios.create(apiConfig);
|
||||
|
||||
// Setup interceptors
|
||||
setupInterceptors(this.axios);
|
||||
setupInterceptors(this.axiosInstance);
|
||||
|
||||
// Add request interceptor
|
||||
this.axiosInstance.interceptors.request.use(
|
||||
async (config) => {
|
||||
const token = await auth.currentUser?.getIdToken();
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Add response interceptor
|
||||
this.axiosInstance.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
const originalRequest = error.config;
|
||||
|
||||
// Check if error is 401 and we haven't tried to refresh token yet
|
||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||
originalRequest._retry = true;
|
||||
|
||||
try {
|
||||
// Force token refresh
|
||||
const user = auth.currentUser;
|
||||
if (user) {
|
||||
const newToken = await user.getIdToken(true);
|
||||
originalRequest.headers.Authorization = `Bearer ${newToken}`;
|
||||
localStorage.setItem('TOKEN', newToken);
|
||||
return this.axiosInstance(originalRequest);
|
||||
}
|
||||
} catch (refreshError) {
|
||||
return Promise.reject(refreshError);
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public static getInstance(): ApiService {
|
||||
@@ -40,7 +83,7 @@ class ApiService {
|
||||
config.data = data;
|
||||
}
|
||||
|
||||
const response = await this.axios.request<T>(config);
|
||||
const response = await this.axiosInstance.request<T>(config);
|
||||
|
||||
return {
|
||||
data: response.data,
|
||||
|
@@ -29,4 +29,5 @@ export interface RequestOptions {
|
||||
headers?: Record<string, string>;
|
||||
params?: Record<string, any>;
|
||||
timeout?: number;
|
||||
responseType? : 'json' | 'blob' | 'text';
|
||||
}
|
10
src/services/api/userApi.ts
Normal file
10
src/services/api/userApi.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Pet } from '../../types/Pet';
|
||||
import { ApiService } from './index';
|
||||
|
||||
// Get API service instance
|
||||
const api = ApiService.getInstance();
|
||||
|
||||
export async function fetchPets(): Promise<Pet[]> {
|
||||
const response = await api.get<Pet[]>('/api/v1/pet');
|
||||
return response.data;
|
||||
}
|
40
src/services/auth/auth.ts
Normal file
40
src/services/auth/auth.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
signInWithEmailAndPassword,
|
||||
createUserWithEmailAndPassword,
|
||||
signOut as firebaseSignOut,
|
||||
User,
|
||||
GoogleAuthProvider,
|
||||
signInWithPopup
|
||||
} from 'firebase/auth';
|
||||
import { auth } from '../../config/firebase';
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
const userCredential = await signInWithEmailAndPassword(auth, email, password);
|
||||
const token = await userCredential.user.getIdToken();
|
||||
localStorage.setItem('TOKEN', token);
|
||||
return token;
|
||||
}
|
||||
|
||||
export async function register(email: string, password: string): Promise<string> {
|
||||
const userCredential = await createUserWithEmailAndPassword(auth, email, password);
|
||||
const token = await userCredential.user.getIdToken();
|
||||
localStorage.setItem('TOKEN', token);
|
||||
return token;
|
||||
}
|
||||
|
||||
export async function signOut(): Promise<void> {
|
||||
await firebaseSignOut(auth);
|
||||
localStorage.removeItem('TOKEN');
|
||||
}
|
||||
|
||||
export function getCurrentUser(): User | null {
|
||||
return auth.currentUser;
|
||||
}
|
||||
|
||||
export async function signInWithGoogle(): Promise<string> {
|
||||
const provider = new GoogleAuthProvider();
|
||||
const userCredential = await signInWithPopup(auth, provider);
|
||||
const token = await userCredential.user.getIdToken();
|
||||
localStorage.setItem('TOKEN', token);
|
||||
return token;
|
||||
}
|
31
src/types/GameItem.ts
Normal file
31
src/types/GameItem.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export interface GameItem {
|
||||
id: number;
|
||||
name: string;
|
||||
type: ItemType;
|
||||
rarity: ItemRarity;
|
||||
description: string;
|
||||
price: number;
|
||||
effect: string;
|
||||
equipTarget: ItemEquipTarget;
|
||||
}
|
||||
|
||||
export enum ItemType {
|
||||
Material = 'Material',
|
||||
Consumable = 'Consumable',
|
||||
Equipment = 'Equipment'
|
||||
}
|
||||
|
||||
export enum ItemRarity {
|
||||
Common = 'Common', // White
|
||||
Uncommon = 'Uncommon', // Green
|
||||
Rare = 'Rare', // Blue
|
||||
Legendary = 'Legendary' // Purple
|
||||
}
|
||||
|
||||
export enum ItemEquipTarget {
|
||||
None = 'None',
|
||||
Head = 'Head',
|
||||
Body = 'Body',
|
||||
Legs = 'Legs',
|
||||
Weapon = 'Weapon'
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
import { PetBasicAction, PetGatherAction } from "./PetUpdateActionRequest";
|
||||
import { PetBasicAction, PetGatherAction } from "./PetAction";
|
||||
|
||||
export type PetClass = 'FOREST_SPIRIT' | 'OCEAN_GUARDIAN' | 'FIRE_ELEMENTAL' | 'MYTHICAL_BEAST' | 'SHADOW_WALKER' | 'CYBER_PET' | 'BIO_MECHANICAL';
|
||||
|
||||
@@ -25,9 +25,19 @@ export interface Pet {
|
||||
stats: PetStats;
|
||||
resources: Resources;
|
||||
level: number;
|
||||
petBasicAction: PetBasicAction;
|
||||
basicActionCooldown: Date;
|
||||
health: number;
|
||||
maxHealth: number;
|
||||
petGatherAction: PetGatherAction;
|
||||
basicActionCooldown: Date;
|
||||
petBasicAction: PetBasicAction;
|
||||
skillPoints: number;
|
||||
inventory: Inventory;
|
||||
}
|
||||
|
||||
export type InvItemInteraction = 'EQUIP' | 'UNEQUIP' | 'USE' | 'DROP';
|
||||
export interface Inventory {
|
||||
items: number[];
|
||||
capacity: number;
|
||||
}
|
||||
|
||||
export interface PetClassInfo {
|
||||
|
22
src/types/PetAction.ts
Normal file
22
src/types/PetAction.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export type PetBasicAction = 'UNKNOWN' | 'FEED' | 'PLAY' | 'SLEEP';
|
||||
export type PetGatherAction = 'IDLE' | 'GATHERING_WISDOM' | 'GATHERING_GOLD' | 'GATHERING_FOOD' | 'EXPLORE' | 'BATTLE';
|
||||
|
||||
export interface PetUpdateActionRequest {
|
||||
basicAction?: PetBasicAction;
|
||||
gatherAction?: PetGatherAction;
|
||||
}
|
||||
|
||||
export interface PetActionGathered {
|
||||
petId: string;
|
||||
resource: string;
|
||||
itemId: number;
|
||||
amount: number;
|
||||
gameItem: PetGatheredItem;
|
||||
}
|
||||
|
||||
export interface PetGatheredItem {
|
||||
id: number;
|
||||
name: number;
|
||||
type: string;
|
||||
rarity: string;
|
||||
}
|
@@ -1,6 +1,4 @@
|
||||
import { PetClass } from "./Pet";
|
||||
|
||||
export interface PetCreationRequest {
|
||||
name: string;
|
||||
class: PetClass;
|
||||
class: string;
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
export type PetBasicAction = 'UNKNOWN' | 'FEED' | 'PLAY' | 'SLEEP';
|
||||
export type PetGatherAction = 'IDLE' | 'GATHERING_WISDOM' | 'GATHERING_GOLD' | 'GATHERING_FOOD';
|
||||
|
||||
export interface PetUpdateActionRequest {
|
||||
basicAction?: PetBasicAction;
|
||||
gatherAction?: PetGatherAction;
|
||||
}
|
34
src/types/Skills.ts
Normal file
34
src/types/Skills.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
export type SkillType = 'GROUND' | 'GRAND';
|
||||
|
||||
export interface Skill {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
type: SkillType;
|
||||
skillRequirements: SkillRequirement[];
|
||||
icon: string;
|
||||
skillsIdRequired: number[] | null;
|
||||
effects: SkillEffect[];
|
||||
}
|
||||
|
||||
export interface SkillRequirement {
|
||||
cost: number;
|
||||
resource: string;
|
||||
}
|
||||
|
||||
export interface SkillEffect {
|
||||
id: number;
|
||||
skillId: number;
|
||||
tier: SkillTier;
|
||||
effect: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export type SkillTier = 'I' | 'II' | 'III';
|
||||
export interface PetSkill {
|
||||
id: number;
|
||||
petId: string;
|
||||
skillId: number;
|
||||
skill: Skill;
|
||||
currentTier: SkillTier;
|
||||
}
|
@@ -1,34 +1,29 @@
|
||||
import { PetGatherAction } from '../types/PetUpdateActionRequest';
|
||||
import { PetGatherAction } from '../types/PetAction';
|
||||
|
||||
export function isGatheringAction(action: PetGatherAction): boolean {
|
||||
return action.startsWith('GATHERING_');
|
||||
}
|
||||
|
||||
export function getResourceFromAction(action: string): string | null {
|
||||
if (!action) return null;
|
||||
|
||||
const patterns = ['GATHERING_', 'EXPLORING_', 'BATTLE_'];
|
||||
for (const pattern of patterns) {
|
||||
if (action.startsWith(pattern)) {
|
||||
return action.replace(pattern, '').toLowerCase();
|
||||
export function isActionActive(currentAction: PetGatherAction, actionType: 'gather' | 'explore' | 'battle'): boolean {
|
||||
if (actionType === 'gather') {
|
||||
return currentAction.startsWith('GATHERING_');
|
||||
}
|
||||
if (actionType === 'explore') {
|
||||
return currentAction === 'EXPLORE';
|
||||
}
|
||||
if (actionType === 'battle') {
|
||||
return currentAction === 'BATTLE';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getResourceFromAction(action: PetGatherAction): string | null {
|
||||
if (action.startsWith('GATHERING_')) {
|
||||
return action.replace('GATHERING_', '').toLowerCase();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function formatResourceName(resource: string): string {
|
||||
return resource.charAt(0).toUpperCase() + resource.slice(1);
|
||||
}
|
||||
|
||||
export function isActionActive(action: string, actionType: string): boolean {
|
||||
if (!action) return false;
|
||||
|
||||
const actionMap = {
|
||||
'gather': 'GATHERING_',
|
||||
'explore': 'EXPLORING_',
|
||||
'battle': 'BATTLE_'
|
||||
};
|
||||
|
||||
return action.startsWith(actionMap[actionType] || '');
|
||||
return resource.charAt(0).toUpperCase() + resource.slice(1).toLowerCase();
|
||||
}
|
Reference in New Issue
Block a user