
MECHA BUlLDER ã¬ã®ã¥ã¬ãŒã·ã§ã³1.0ç
ããããããã«ç©ã§ãã
ãããªã³ã°ãã¿ã§ããã
ã²ãŒã ãäœããŸãã·ãªãŒãºãå å®¹ãæ³šæããããããªã£ãããŒãžã§ã³ãïŒç¬ïŒ
æ¬åœã«Gemini3ã¯åªç§ããã©ãã«ç¡ãâŠãç¡ããã©ããããé 匵ãã
ã¯ããŒãããé ŒããªããŠã宿ãããã®ã¯2.5ãšå€§éããæ¬åœã«åãã§ãã
ã»ãŒåæãã©ã³ã¹ã¯AIãæ±ºããŠãã®ã§ãã©ã³ã¹æèŠã¯AIåŽã®ãã®æŒåºã¯ã€ãã€ã远å ããŸãããã©
ãªã®ã§ãã®åŸãå¯åãããŠã¹ããŒãªãŒã¢ãŒããšãäœããã©ããããã¶ããããã¯ãã®ã¬ã®ã¥ã¬ãŒã·ã§ã³ã§å¯åãããŠãå¥ã«ã¹ããŒãªãŒã¢ãŒãçãäœããšãããªïŒ
ãšãããã¯ãã°ããã¯ãã®ãã©ã³ã¹ã§åããèŠãŸãã
(ãããèŠãŠãäœè£ãªããã ãã©âŠã)
äžå¿ãä»ã®äººãšå¯ŸæŠå¯èœãããã§ãã
ãã¡ãã¯ãŸã æªãã¹ã
èªåã®æ©äœå士ã§ã®åäœãŸã§ç¢ºèªããŠãŸãã
ã¬ã®ã¥ã¬ãŒã·ã§ã³å€æŽã§åãã¬ã®ã¥ã¬ãŒã·ã§ã³ã®ããŒãžã§ã³ãšããæŠããªãããã§ãã
ãäºæ¿äžããã

MECHA BUILDER ãŠãŒã¶ãŒããã¥ã¢ã«
ãã®ã¢ããªã¯ãç¬èªã®ããããïŒã¡ã«ïŒãæ§ç¯ããä»ã®ãŠãŒã¶ãŒãäœæãããããããšå¯ŸæŠãããããã®ã·ãã¥ã¬ãŒã·ã§ã³ããŒã«ã§ãã

1. ã¢ããªã®ã¢ãŒãç»é¢äžéšã®ãã¿ã³ã§3ã€ã®ã¢ãŒããåãæ¿ããŸãã
ã¢ã€ã³ã³ã¢ãŒãåæŠèŠ
âïžããããæ§ç¯ (BUILD)ããŒããååãã»ãªããèšå®ãããããããä¿åããŸãã
âïžå¯ŸæŠçžæéžæ (SELECT_OPPONENT)èªåã®ãããããšã察æŠçžæãéžæããŸãã
â¡ïžããã«ã·ãã¥ã¬ãŒã·ã§ã³ (BATTLE)éžæãã2äœã®ããããã察æŠãããæŠéãã°ã確èªããŸãã
2. âïž ããããæ§ç¯ (BUILD)ããããã®èšèšãè¡ãã¢ãŒãã§ãã
2.1. æ©äœæ
å ±ã®èšå®æ©äœå / ãã€ãããå: ãããããšãã€ãããã®ååãå
¥åããŸãã
ããŒãéžæ: HEAD, BODY, ARMS, LEGS ã®åããŒããéžæããŸããããŒããéžã¶ãšãäžéšã®ãç·åã¹ããŒã¿ã¹ãããªã¢ã«ã¿ã€ã ã§å€åããŸããéèŠ: SPD (ã¹ããŒã)ãé«ãã»ã©ãæŠéäžã®è¡åé ãææ°ã«åœ±é¿ããŸãã

2.2. ãã€ãããã»ãªãèšå®æŠéäžã«ãã€ããããçºèšããã»ãªããèšå®ããŸãã
ð¡ è€æ°ã®ã»ãªãåè£ã â³ ã§åºåã£ãŠå
¥åããããšã§ãæŠéæã«ã©ã³ãã ã«éžã°ããŸããïŒäŸ: ãã£ãããŒïŒâ³ãããããïŒâ³é¿ããïŒïŒ
2.3. ããããã®ä¿åããŒãéžæãšã»ãªãèšå®ãå®äºããããããããããä¿åããã¿ã³ãæŒããŠãããŒã¿ãã¯ã©ãŠãïŒFirestoreïŒã«ç»é²ããŸããä¿åãããããããã¯ãããªãã®ç»é²ãããããªã¹ãã«è¡šç€ºããã察æŠçžæãšããŠä»ã®ãŠãŒã¶ãŒã«ãå ¬éãããŸãã
3. âïž å¯ŸæŠçžæéžæ (SELECT_OPPONENT)ããã«ãéå§ããããã®æºåã¢ãŒãã§ãã
èªåã®ããããã®éžæ: äžéšã®ãªã¹ãããã察æŠã«äœ¿çšããããªãã®ãããããã¯ãªãã¯ããŠéžæããŸãã
察æŠçžæã®ãªã¹ãæŽæ°: å³äžã®ãæŽæ°ããã¿ã³ãæŒããŠãä»ã®ãŠãŒã¶ãŒãç»é²ããææ°ã®ãããããªã¹ããååŸããŸãã
察æŠçžæã®éžæ: ãªã¹ããã察æŠããããçžæã®ãããããã¯ãªãã¯ããŠéžæããŸãã
ããã«éå§: èªåã®ãããããšå¯ŸæŠçžæã®äž¡æ¹ãéžæãããç¶æ
ã§ããããã«éå§ïŒããã¿ã³ãæŒããšãBATTLEã¢ãŒãã«ç§»è¡ããŸãã


4. â¡ïž ããã«ã·ãã¥ã¬ãŒã·ã§ã³ (BATTLE)æŠéãèªåã§ã·ãã¥ã¬ãŒã·ã§ã³ãããçµæããªã¢ã«ã¿ã€ã ã§è¡šç€ºãããŸãã
æŠéããžãã¯:åããããã®SPDå€ã«å¿ããŠãè¡åïŒã¢ã¯ã·ã§ã³ïŒã«å¿
èŠãªã«ãŠã³ãã貯ãŸããŸããã«ãŠã³ããäžå®å€ïŒ30ïŒã«éãããšãæ»ææš©ãçºçããŸãã
æ»ææã«ã¯ãATKãšDEFã«åºã¥ãããã¡ãŒãžãèšç®ãããä¹±æ°èŠçŽ ãå«ãŸãããããåãçµã¿åããã§ãçµæãå€ããããšããããŸãã
æŠéãã°:æŠéã®é²è¡ç¶æ³ïŒæ»æããã¡ãŒãžããã€ãããã®ã»ãªãïŒãæç³»åã§è¡šç€ºãããŸãã
çµæ:ã©ã¡ããã®HPããŒãã«ãªãããã¹ãããæ°ãæå€§å€ïŒ50ïŒã«éãããšæŠéçµäºã§ããåè
ã衚瀺ãããŸãã
5. éèŠãªæ³šæäºé ããŒã¿ä¿å: ããããããŒã¿ã¯ã¯ã©ãŠãïŒFirestoreïŒã«ä¿åãããããããã©ãŠã¶ãéããŠãããŒã¿ã倱ãããããšã¯ãããŸãããå ¬éããŒã¿: äœæã»ä¿åãããããããã¯ãããªãã®ãŠãŒã¶ãŒIDãšãšãã«å šãŠãŒã¶ãŒã«å ¬éããã察æŠçžæãšããŠéžæå¯èœã«ãªããŸãã
å°ãå¶äœè
ã¯ãã©ã³ã¹èª¿æŽã®çºã«ã¬ã®ã¥ã¬ãŒã·ã§ã³ãä»»æã§å€æŽããããšããããŸãã(ããŒã¿ãã¬ã®ã¥ã¬ãŒã·ã§ã³å€æŽã§åæåãåèšå®ãå¿
èŠã«ãªãå ŽåãããåŸãŸã)
ãããå«ã䜿çšããããšã«ãã責任ã¯äžåè² ããŸãããèªå·±è²¬ä»»ã§ã楜ãã¿ãã ããã
転売çã埡èªèº«ã§å¶äœè ãåä¹ãçã¯ãæ¢ããã ããã
ãGemini ãCanvasã¢ãŒãã§ã®èµ·åãæ³å®ããŠããŸãã(ãã¹ãç°å¢)
ãCanvasã¢ãŒãã§åŒµãä»ããŠãã ããã
ãããšã¯æ©äœãäœã£ãŠããããªã³ã°éå§
ãæ©äœã¯è¿œå ä¿®æ£ãåºæ¥ãªãã®ã§äœã£ããç»é²ãããããã§å®æåœ¢ãšãªããŸãã
ãå°è©çãåŸä¿®æ£ã¯åºæ¥ãªãã®ã§å¿æ®ãã®ç¡ãããã«(察æŠããŒã¿ãšãªãã®ã§éäžã§å€ãããšãã©ãã«ã«ãªãããïŒå€å仿§ãªã ãããã§ãã)
ã
ãimport React, { useState, useEffect, useMemo } from 'react';
// ðš ä¿®æ£: URLåç
§ã§ã¯ãªããã¢ãžã¥ãŒã«åã«ããã€ã³ããŒãã«å€æŽ
import { initializeApp } from 'firebase/app';
import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from 'firebase/auth';
import { getFirestore, doc, setDoc, addDoc, deleteDoc, onSnapshot, collection, query, where, getDocs } from 'firebase/firestore';
import { Settings, Sword, User, Loader2, Save, Trash2, Zap, Heart, Shield, MessageSquare, Mic, FastForward } from 'lucide-react';
// --- FirebaseãšTailwind CSSã䜿ã£ãããããã¢ã»ã³ããªïŒããã«ã¢ãªãŒã ---
// --- 1. ã²ãŒã èšå®ãšããŒãããŒã¿ ---
const ROBOT_PARTS = {
head: [
{ name: 'ã»ã³ãµãŒããã', stats: { hp: 10, atk: 5, spd: 8 }, icon: 'ðïž', color: 'bg-yellow-500' },
{ name: 'éè£
ãã€ã¶ãŒ', stats: { hp: 20, atk: 2, spd: 3 }, icon: 'ð¡ïž', color: 'bg-gray-400' },
{ name: 'ãã£ãã³ãã«ã¡ãã', stats: { hp: 5, atk: 18, spd: 5 }, icon: 'ð£', color: 'bg-red-600' },
],
body: [
{ name: 'ã³ã¢ãã¬ãŒã ', stats: { hp: 30, def: 5, spd: 10 }, icon: 'âïž', color: 'bg-blue-500' },
{ name: 'ãããŒã·ã£ãŒã·', stats: { hp: 40, def: 8, spd: 2 }, icon: 'ðïž', color: 'bg-slate-700' },
{ name: 'ã¹ããŒãã·ã§ã«', stats: { hp: 10, def: 2, spd: 15 }, icon: 'ðš', color: 'bg-green-500' },
],
arms: [
{ name: 'ãã©ã¹ã¿ãŒã¢ãŒã ', stats: { atk: 18, spd: 5 }, icon: 'ð¥', color: 'bg-red-500' },
{ name: 'ã·ãŒã«ããã£ã¹ã', stats: { atk: 5, spd: 8 }, icon: 'âïž', color: 'bg-gray-500' },
{ name: 'ã°ãªããã¯ããŒ', stats: { atk: 10, spd: 12 }, icon: 'ðŠŸ', color: 'bg-orange-500' },
],
legs: [
{ name: 'ãã©ãã¯ããŒã¹', stats: { def: 10, spd: 1 }, icon: 'ð', color: 'bg-yellow-700' },
{ name: 'ãžã§ããããŒã¹ã¿ãŒ', stats: { def: 5, spd: 15 }, icon: 'ð', color: 'bg-cyan-500' },
{ name: 'æšæºè', stats: { def: 2, spd: 8 }, icon: 'ð', color: 'bg-gray-300' },
]
};
const DEFAULT_BATTLE_MESSAGES = {
onHit: "ãã£ããã®çšåºŠïŒâ³ãŸã ãŸã ãããïŒâ³æ³å®å
ã ã",
onAttack: "ãã£ãããŒïŒâ³çãæã€ïŒâ³äžæã§ä»çããïŒ",
onWin: "åå©ïŒããããªã¬ãã¡ã®åã ïŒâ³æªãæããªãïŒâ³æŠãã®ç¶ãã¯ãŸãä»åºŠã ïŒ",
onLose: "ã¡ãããã...次ã¯åã€ïŒâ³ãŸããè² ãããªããŠ...â³ããŒã¿ãè¶³ããªãã£ããã",
};
const MESSAGE_LABELS = {
onHit: "被ãã¡ãŒãžæ",
onAttack: "æ»ææ",
onWin: "å婿",
onLose: "æåæ",
};
const MESSAGE_DELIMITER = 'â³';
const ACTION_THRESHOLD = 30; // æ»æã«å¿
èŠãªã«ãŠã³ã
// å
±éããŠäœ¿çšããFirebase/Firestoreã®å€æ°
let dbInstance = null;
let authInstance = null;
// --- 2. ä¹±æ°ã¹ããŒã¿ã¹èšç®ãã«ã㌠---
const calculateRandomStat = (baseStat) => {
if (baseStat <= 0) return 0;
// ååã確å®å€
const fixedPart = Math.floor(baseStat / 2);
// æ®ãã®ååãä¹±æ°ã§æ±ºå® (0ããrandomMaxãŸã§ã®ä¹±æ°)
const randomMax = baseStat - fixedPart;
const randomPart = Math.floor(Math.random() * (randomMax + 1));
return fixedPart + randomPart;
};
// --- 3. ããŒã¿æ£èŠåãã«ã㌠(匷å): å
šãŠã®ãã¹ããããæ§é ãä¿èšŒããã¬ã³ããªã³ã°ãšã©ãŒãé²ã ---
const normalizeRobotData = (data) => {
// 1. Stats safety (totalStatsãååšããhp/atk/def/spdã0以äžã§ããããšãä¿èšŒ)
const safeStats = data.totalStats || {};
const normalizedStats = {
hp: safeStats.hp || 0,
atk: safeStats.atk || 0,
def: safeStats.def || 0,
spd: safeStats.spd || 0,
};
// 2. Messages safety (battleMessagesãååšããããšãä¿èšŒ)
const safeMessages = data.battleMessages || DEFAULT_BATTLE_MESSAGES;
// 3. Parts safety (partsãªããžã§ã¯ããšãã®åäŸãå
šãŠãªããžã§ã¯ãã§ããããšãä¿èšŒ)
const rawParts = data.parts || {};
const normalizedParts = {
// head, body, arms, legs ã null/undefined ã®å Žåã空ã®ãªããžã§ã¯ã {} ãå²ãåœãŠã
head: rawParts.head || {},
body: rawParts.body || {},
arms: rawParts.arms || {},
legs: rawParts.legs || {},
};
return {
...data,
totalStats: normalizedStats,
battleMessages: safeMessages,
parts: normalizedParts,
pilotName: data.pilotName || 'åç¡ãã®ãã€ããã',
};
};
const App = () => {
// --- ç¶æ
管ç ---
const [mode, setMode] = useState('BUILD');
const [userId, setUserId] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// ããããæ§ç¯çš
const [robotName, setRobotName] = useState('ãã€ãããã-01');
const [pilotName, setPilotName] = useState('ã«ãŒããŒã»ãšãŒã¹');
const [selectedParts, setSelectedParts] = useState({
head: ROBOT_PARTS.head[0],
body: ROBOT_PARTS.body[0],
arms: ROBOT_PARTS.arms[0],
legs: ROBOT_PARTS.legs[0],
});
const [customMessages, setCustomMessages] = useState(DEFAULT_BATTLE_MESSAGES);
// 察æŠçšããŒã¿
const [myRobots, setMyRobots] = useState([]);
const [opponentRobots, setOpponentRobots] = useState([]);
const [selectedMyRobot, setSelectedMyRobot] = useState(null);
const [selectedOpponent, setSelectedOpponent] = useState(null);
// ããã«ã·ãã¥ã¬ãŒã·ã§ã³çµæ
const [battleLog, setBattleLog] = useState([]);
const [battleResult, setBattleResult] = useState(null);
const [battleResultId, setBattleResultId] = useState(null);
const [isSimulating, setIsSimulating] = useState(false);
const [isAuthReady, setIsAuthReady] = useState(false); // èªèšŒæºåå®äºãã©ã°
const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-robot-app';
const collectionPath = `artifacts/${appId}/public/data/robots`;
// --- 4. FirebaseåæåãšèªèšŒ ---
useEffect(() => {
let app;
let authUnsubscribe;
try {
// ðš é²åŸ¡ç匷å: Firebase Configã®å®å
šãªããŒã¹
let firebaseConfig = {};
const configString = typeof __firebase_config !== 'undefined' ? __firebase_config : null;
if (configString) {
try {
firebaseConfig = JSON.parse(configString);
} catch (e) {
console.error("Firebase Config JSON Parse Error:", e);
setError("Firebaseèšå®ãã¡ã€ã«ã®è§£æã«å€±æããŸããã");
setLoading(false);
return;
}
}
// Firebaseåæå
app = initializeApp(firebaseConfig);
dbInstance = getFirestore(app);
authInstance = getAuth(app);
authUnsubscribe = onAuthStateChanged(authInstance, async (user) => {
if (user) {
setUserId(user.uid);
} else {
try {
if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) {
await signInWithCustomToken(authInstance, __initial_auth_token);
} else {
await signInAnonymously(authInstance);
}
} catch (e) {
// èªèšŒã«å€±æããŠãã¢ããªã¯åããããã«ãšã©ãŒã¯ã»ããããªãïŒå¿åèªèšŒããããã¯ãããå Žåãªã©ïŒ
console.warn("Firebase Auth Failed (falling back to unauth state):", e);
}
}
// èªèšŒç¶æ
ã®ç¢ºèªãå®äº
setIsAuthReady(true);
setLoading(false);
});
} catch (e) {
console.error("Firebase Initialization Error:", e);
setError(`Firebaseã®åæåã«å€±æããŸããããšã©ãŒ: ${e.message}`);
setLoading(false);
}
return () => {
if (authUnsubscribe) authUnsubscribe();
};
}, []);
// --- 5. ããããããŒã¿ã®ããŒãïŒFirestoreïŒ ---
useEffect(() => {
// ðš 峿 Œãªãã§ãã¯: DBã€ã³ã¹ã¿ã³ã¹ãšèªèšŒãå®äºãããŸã§åŠçãã¹ããã
if (!isAuthReady || !dbInstance || !userId) return;
const loadMyRobots = () => {
try {
const q = query(collection(dbInstance, collectionPath), where("userId", "==", userId));
const unsubscribe = onSnapshot(q, (snapshot) => {
// 匷åãããæ£èŠååŠçãå®è¡
const bots = snapshot.docs.map(doc => normalizeRobotData({ id: doc.id, ...doc.data() }));
setMyRobots(bots);
// çŸåšéžæäžã®ããããã®ããŒã¿æŽæ°
if (selectedMyRobot) {
const updatedBot = bots.find(b => b.id === selectedMyRobot.id);
if (updatedBot) setSelectedMyRobot(updatedBot);
}
}, (err) => {
console.error("My Robot Snapshot Error:", err);
setError(`FirestoreããŒã¿ååŸãšã©ãŒ: ${err.message}`);
});
return unsubscribe;
} catch (e) {
console.error("My Robot Load Error:", e);
// ãã®ãšã©ãŒã¯èªèšŒåŸã§DBã€ã³ã¹ã¿ã³ã¹ãããã¯ããªã®ã§ãããæ·±å»ãªãšã©ãŒ
setError(`èªåã®ããããã®èªã¿èŸŒã¿ã«å€±æããŸããããšã©ãŒ: ${e.message}`);
}
};
return loadMyRobots();
}, [isAuthReady, userId]);
const loadAllPublicRobots = async () => {
if (!isAuthReady || !dbInstance) return;
setLoading(true);
try {
const q = query(collection(dbInstance, collectionPath));
const snapshot = await getDocs(q);
// 匷åãããæ£èŠååŠçãå®è¡
const bots = snapshot.docs.map(doc => normalizeRobotData({ id: doc.id, ...doc.data() }));
setOpponentRobots(bots);
setSelectedOpponent(null);
setLoading(false);
} catch (e) {
console.error("All Public Robot Load Error:", e);
setError(`å
¬éããããã®èªã¿èŸŒã¿ã«å€±æããŸããããšã©ãŒ: ${e.message}`);
setLoading(false);
}
};
// --- 6. ã¹ããŒã¿ã¹èšç® (SPDã远å ) ---
const calculateStats = useMemo(() => {
const stats = { hp: 0, atk: 0, def: 0, spd: 0 };
Object.values(selectedParts).forEach(part => {
// å®å
šæ§ã®ããã« part.stats ãååšããããšããã§ãã¯
const partStats = part.stats || {};
stats.hp += partStats.hp || 0;
stats.atk += partStats.atk || 0;
stats.def += partStats.def || 0;
stats.spd += partStats.spd || 0;
});
return stats;
}, [selectedParts]);
// --- 7. ããããä¿åïŒFirestoreãžã®æžã蟌ã¿ïŒ ---
const handleSaveRobot = async () => {
if (!dbInstance || !userId || !isAuthReady) {
setError("Firebaseæ¥ç¶ãŸãã¯ãŠãŒã¶ãŒIDãååŸã§ããŠããŸãããèªèšŒã®å®äºãåŸ
ã£ãŠãã ããã");
return;
}
if (!robotName.trim() || !pilotName.trim()) {
setError("ããããåãšãã€ãããåãå
¥åããŠãã ããã");
return;
}
setLoading(true);
setError(null);
// ä¿åæã«ææ°ã®ããŒãããŒã¿æ§é ãFirestoreã«éã
const newRobot = {
userId: userId,
name: robotName,
pilotName: pilotName,
parts: {
head: selectedParts.head,
body: selectedParts.body,
arms: selectedParts.arms,
legs: selectedParts.legs,
},
totalStats: calculateStats,
battleMessages: customMessages,
createdAt: Date.now(),
};
try {
const existingBot = myRobots.find(b => b.name === robotName);
if (existingBot) {
await setDoc(doc(dbInstance, collectionPath, existingBot.id), newRobot);
} else {
await addDoc(collection(dbInstance, collectionPath), newRobot);
}
setLoading(false);
} catch (e) {
console.error("Save Robot Error:", e);
setError(`ããããã®ä¿åã«å€±æããŸããããšã©ãŒ: ${e.message}`);
setLoading(false);
}
};
// --- 8. ããããã®åé€ ---
const handleDeleteRobot = async (botId) => {
if (!dbInstance || !isAuthReady) return;
try {
await deleteDoc(doc(dbInstance, collectionPath, botId));
if (selectedMyRobot && selectedMyRobot.id === botId) {
setSelectedMyRobot(null);
}
if (selectedOpponent && selectedOpponent.id === botId) {
setSelectedOpponent(null);
}
} catch (e) {
console.error("Delete Robot Error:", e);
setError(`ããããã®åé€ã«å€±æããŸããããšã©ãŒ: ${e.message}`);
}
};
// --- 9. æ©äœã®ç¹åŸŽãè©äŸ¡ãããã«ããŒé¢æ° (ãã¬ãŒã·ã§ã³çš) ---
const generateRobotDescription = (bot) => {
const stats = bot.totalStats;
const { hp, atk, def, spd } = stats;
let features = [];
let assessment = "";
// è©äŸ¡ããžã㯠(æ«å®çãªåºæºå€: HP=50, ATK=20, DEF=15, SPD=30)
const isHighHP = hp >= 50;
const isHighATK = atk >= 20;
const isHighDEF = def >= 15;
const isHighSPD = spd >= 35;
const isLowSPD = spd <= 20;
if (isHighSPD && isHighATK) {
assessment = "é»å
ç³ç«ã®ãããïŒã¢ãŠã§ã€å";
} else if (isHighHP && isHighDEF && isLowSPD) {
assessment = "飿»äžèœã®èŠå¡åïŒæ©ååé£ããïŒ";
} else if (isHighATK) {
assessment = "æ»æåã«ç¹åããé«ç«åã¢ã¿ãã«ãŒ";
} else if (isHighSPD) {
assessment = "ã¹ããŒãéèŠã®é«éæŠéæ©";
} else if (isHighDEF) {
assessment = "é²åŸ¡åã«ç¹åããéè£
ç²å";
} else {
assessment = "æšæºçãªæ§èœãæã€ãã©ã³ã¹å";
}
features.push(`ãç¹åŸŽè©äŸ¡ã${assessment}`);
const partDetails = [];
// partsã®ãã¹ããããæ§é ãä¿èšŒãããŠãããããå®å
šã«ã¢ã¯ã»ã¹å¯èœ
if (bot.parts?.body?.name === 'ã¹ããŒãã·ã§ã«' || bot.parts?.legs?.name === 'ãžã§ããããŒã¹ã¿ãŒ') partDetails.push("é«éç§»åãå¯èœã«ããããŒã¹ã¿ãŒè£
å");
if (bot.parts?.head?.name === 'éè£
ãã€ã¶ãŒ' && bot.parts?.legs?.name === 'ãã©ãã¯ããŒã¹') partDetails.push("培åºçãªé²åŸ¡ãšåŒãæãã«æ©ååãç ç²");
if (partDetails.length > 0) {
features.push(`ãããŒãã${partDetails.join('ã')}`);
}
return features.join(" / ");
};
// --- 10. ããã«ã·ãã¥ã¬ãŒã·ã§ã³ããžã㯠(ä¹±æ°ãã¡ãŒãžãšã¿ã€ã ã¹ã©ã€ã·ã³ã°ãå°å
¥) ---
const simulateBattle = (botA, botB) => {
const log = [];
// totalStatsã¯æ£èŠåã§ååšãä¿èšŒãããŠããããå®å
šã«ã¢ã¯ã»ã¹å¯èœ
const getStat = (bot, stat) => bot.totalStats?.[stat] || 0;
const getMessage = (bot, type) => {
const messageString = bot.battleMessages[type] || DEFAULT_BATTLE_MESSAGES[type];
const messages = messageString.split(MESSAGE_DELIMITER)
.map(m => m.trim())
.filter(m => m.length > 0);
if (messages.length === 0) {
return DEFAULT_BATTLE_MESSAGES[type].split(MESSAGE_DELIMITER)[0].trim() || "ïŒã¡ãã»ãŒãžãªãïŒ";
}
const randomIndex = Math.floor(Math.random() * messages.length);
return messages[randomIndex];
};
const getPilotName = (bot) => bot.pilotName || 'åç¡ãã®ãã€ããã';
let hpA = getStat(botA, 'hp') || 10;
let hpB = getStat(botB, 'hp') || 10;
const spdA = getStat(botA, 'spd');
const spdB = getStat(botB, 'spd');
let step = 1;
const MAX_STEPS = 50;
let countA = 0;
let countB = 0;
const performAttack = (attacker, defender, hpDefender, isA, currentStep) => {
const atkerName = attacker.name;
const defrName = defender.name;
const atkerPilot = getPilotName(attacker);
const defrPilot = getPilotName(defender);
const calculatedAtk = calculateRandomStat(getStat(attacker, 'atk'));
const calculatedDef = calculateRandomStat(getStat(defender, 'def'));
const rawDamage = calculatedAtk - Math.floor(calculatedDef / 2);
const damage = Math.max(1, rawDamage);
log.push({ type: 'message', name: atkerPilot, text: getMessage(attacker, 'onAttack'), isA: isA });
const prevHp = hpDefender;
const newHp = Math.max(0, hpDefender - damage);
log.push({
step: currentStep,
attacker: atkerName,
defender: defrName,
damage: damage,
atk: calculatedAtk,
def: calculatedDef,
hpA: isA ? hpA : newHp,
hpB: isA ? newHp : hpB,
isA: isA
});
if (damage > 0 && newHp < prevHp) {
log.push({ type: 'message', name: defrPilot, text: getMessage(defender, 'onHit'), isA: !isA });
}
return newHp;
};
// --- ãã¬ãŒã·ã§ã³ (æŠéå) ---
log.push({ message: `--- ããã«ã¢ããŠã³ã¹ ---` });
log.push({
type: 'narration',
text: `éã³ãŒããŒïŒæ©äœå <span class="font-extrabold text-indigo-300">${botA.name}</span>ããã€ããã㯠<span class="font-extrabold text-indigo-300">${getPilotName(botA)}</span>ïŒ`
});
log.push({
type: 'narration',
text: `æ©äœç¹åŸŽ: ${generateRobotDescription(botA)} (HP:${getStat(botA, 'hp')} / ATK:${getStat(botA, 'atk')} / DEF:${getStat(botA, 'def')} / <span class="text-green-400">SPD:${spdA}</span>)`
});
log.push({
type: 'narration',
text: `èµ€ã³ãŒããŒïŒæ©äœå <span class="font-extrabold text-red-300">${botB.name}</span>ããã€ããã㯠<span class="font-extrabold text-red-300">${getPilotName(botB)}</span>ïŒ`
});
log.push({
type: 'narration',
text: `æ©äœç¹åŸŽ: ${generateRobotDescription(botB)} (HP:${getStat(botB, 'hp')} / ATK:${getStat(botB, 'atk')} / DEF:${getStat(botB, 'def')} / <span class="text-green-400">SPD:${spdB}</span>)`
});
log.push({ message: `--- æŠééå§ïŒ (30ã«ãŠã³ãã§ã¢ã¯ã·ã§ã³) ---` });
// --- ã¡ã€ã³ããã«ã«ãŒã ---
while (hpA > 0 && hpB > 0 && step <= MAX_STEPS) {
countA += spdA;
countB += spdB;
let attacksInStep = 0;
while ((countA >= ACTION_THRESHOLD || countB >= ACTION_THRESHOLD) && attacksInStep < 5) {
attacksInStep++;
const canA = countA >= ACTION_THRESHOLD;
const canB = countB >= ACTION_THRESHOLD;
let attacker = null;
let isAAttacking = false;
if (!canA && !canB) break;
// åªå
é äœæ±ºå®ããžãã¯
if (canA && canB) {
if (countA > countB) {
attacker = botA; isAAttacking = true;
} else if (countB > countA) {
attacker = botB; isAAttacking = false;
} else {
if (spdA > spdB) {
attacker = botA; isAAttacking = true;
} else if (spdB > spdA) {
attacker = botB; isAAttacking = false;
} else {
attacker = Math.random() < 0.5 ? botA : botB;
isAAttacking = attacker === botA;
}
}
} else if (canA) {
attacker = botA; isAAttacking = true;
} else if (canB) {
attacker = botB; isAAttacking = false;
}
// æ»æã®å®è¡ãšã«ãŠã³ãã®ãªã»ãã
if (isAAttacking) {
log.push({ type: 'narration', text: `[ã¹ããã ${step} - ${attacksInStep}] <span class="text-indigo-300">${attacker.name}</span> ãã¢ã¯ã·ã§ã³ïŒ (ã«ãŠã³ã:${Math.floor(countA)})` });
hpB = performAttack(botA, botB, hpB, true, step);
countA -= ACTION_THRESHOLD;
} else {
log.push({ type: 'narration', text: `[ã¹ããã ${step} - ${attacksInStep}] <span class="text-red-300">${attacker.name}</span> ãã¢ã¯ã·ã§ã³ïŒ (ã«ãŠã³ã:${Math.floor(countB)})` });
hpA = performAttack(botB, botA, hpA, false, step);
countB -= ACTION_THRESHOLD;
}
if (hpA <= 0 || hpB <= 0) {
log.push({
type: 'narration',
text: `äžæå¿
æ®ºïŒ åè² ããïŒ`
});
break;
}
}
if (hpA <= 0 || hpB <= 0) break;
step++;
}
// åå©å€å®
let winnerName = 'åŒãåã';
let winnerId = null;
if (hpA > 0 && hpB <= 0) {
winnerName = botA.name;
winnerId = botA.id;
log.push({ type: 'message', name: getPilotName(botA), text: getMessage(botA, 'onWin'), isA: true });
log.push({ type: 'message', name: getPilotName(botB), text: getMessage(botB, 'onLose'), isA: false });
} else if (hpB > 0 && hpA <= 0) {
winnerName = botB.name;
winnerId = botB.id;
log.push({ type: 'message', name: getPilotName(botB), text: getMessage(botB, 'onWin'), isA: false });
log.push({ type: 'message', name: getPilotName(botA), text: getMessage(botA, 'onLose'), isA: true });
} else if (step > MAX_STEPS) {
log.push({ message: `--- æéåã (${MAX_STEPS}ã¹ãããå°é) ---` });
if (hpA > hpB) {
winnerName = botA.name;
winnerId = botA.id;
log.push({ type: 'message', name: getPilotName(botA), text: getMessage(botA, 'onWin'), isA: true });
} else if (hpB > hpA) {
winnerName = botB.name;
winnerId = botB.id;
log.push({ type: 'message', name: getPilotName(botB), text: getMessage(botB, 'onWin'), isA: false });
} else {
winnerName = 'æéåã/åŒãåã (HPåå€)';
winnerId = null;
}
}
log.push({ message: `--- 決çïŒ åè
: ${winnerName} (æ®HP A:${hpA}, B:${hpB}) ---` });
return { log, winnerName, winnerId };
};
const startBattle = () => {
if (!selectedMyRobot || !selectedOpponent) return;
setIsSimulating(true);
setBattleLog([]);
setBattleResult(null);
setBattleResultId(null);
const result = simulateBattle(selectedMyRobot, selectedOpponent);
let logIndex = 0;
const interval = setInterval(() => {
if (logIndex < result.log.length) {
setBattleLog(prev => [...prev, result.log[logIndex]]);
logIndex++;
} else {
clearInterval(interval);
setBattleResult(result.winnerName);
setBattleResultId(result.winnerId);
setIsSimulating(false);
}
}, 100);
};
// --- å
±éUIã³ã³ããŒãã³ã (Appå
ã«å®çŸ©ãçŽããŸãã) ---
const StatDisplay = ({ Icon, label, value, color }) => (
<div className="text-center">
<Icon size={24} className={`mx-auto ${color}`} />
<p className="text-xs text-slate-400">{label}</p>
<p className="text-lg font-extrabold text-white">{value}</p>
</div>
);
// --- UIéšå: ãããã衚瀺ã³ã³ããŒãã³ã (倿Žãªã) ---
const RobotCard = ({ robot, isSelected = false, onClick, type = 'my' }) => {
// totalStatsã¯æ£èŠåã«ããååšãä¿èšŒãããŠãã
const stats = robot.totalStats;
const pilotNameDisplay = robot.pilotName || 'åç¡ãã®ãã€ããã';
const StatIcon = ({ Icon, value, color }) => (
<span className={`flex items-center text-xs ${color} font-bold`}>
<Icon size={12} className="mr-1"/> {value}
</span>
);
const RobotVisual = () => (
<div className="flex flex-col items-center justify-center p-2">
{/* robot.parts.head ã¯æ£èŠåã«ãã {} ã®ãããããä¿èšŒãããŠãã */}
<div className={`text-xl ${robot.parts?.head?.color || 'text-white'}`}>{robot.parts?.head?.icon || 'â'}</div>
<div className={`text-3xl ${robot.parts?.arms?.color || 'text-white'} flex items-center`}>
{robot.parts?.arms?.icon || 'â'}
<span className={`text-xl mx-2 ${robot.parts?.body?.color || 'text-white'}`}>{robot.parts?.body?.icon || 'â'}</span>
{robot.parts?.arms?.icon || 'â'}
</div>
<div className={`text-xl ${robot.parts?.legs?.color || 'text-white'}`}>{robot.parts?.legs?.icon || 'â'}</div>
</div>
);
return (
<div
className={`p-4 rounded-xl shadow-lg transition duration-200 cursor-pointer
${isSelected ? 'bg-indigo-700 ring-4 ring-indigo-400 scale-[1.02]' : 'bg-slate-700 hover:bg-slate-600'}
${onClick ? 'hover:scale-[1.02]' : ''}`}
onClick={onClick}
>
<h3 className="text-lg font-extrabold mb-1 truncate" title={robot.name}>{robot.name}</h3>
<p className="text-sm font-semibold text-yellow-300 mb-2 truncate">
<User size={12} className="inline mr-1"/> {pilotNameDisplay}
</p>
<p className={`text-xs mb-2 ${robot.userId === userId ? 'text-indigo-300' : 'text-red-300'}`}>
{robot.userId === userId ? 'äœæè
: ããªã' : `äœæè
: ${robot.userId?.slice(0, 8)}...`}
</p>
<RobotVisual />
<div className="flex justify-between items-center mt-3 border-t border-slate-600 pt-2">
<StatIcon Icon={Heart} value={stats.hp} color="text-red-400"/>
<StatIcon Icon={Zap} value={stats.atk} color="text-yellow-400"/>
<StatIcon Icon={Shield} value={stats.def} color="text-cyan-400"/>
<StatIcon Icon={FastForward} value={stats.spd} color="text-green-400"/>
</div>
{type === 'my' && robot.userId === userId && (
<div className="mt-3 border-t border-slate-600 pt-3 text-right">
<button
onClick={(e) => { e.stopPropagation(); handleDeleteRobot(robot.id); }}
className="text-red-400 hover:text-red-300 transition text-sm flex items-center ml-auto"
>
<Trash2 size={14} className="mr-1" /> åé€
</button>
</div>
)}
</div>
);
};
// --- UI: åã¢ãŒãã®ã¬ã³ããªã³ã°é¢æ° (倿Žãªã) ---
const renderBuildMode = () => (
<div className="p-4 md:p-8 space-y-8 max-w-xl mx-auto">
<h2 className="text-3xl font-extrabold text-indigo-400 flex items-center gap-2">
<Settings size={28} /> ããããæ§ç¯ã¢ãªãŒã
</h2>
<p className="text-slate-400">ããŒããéžã³ãã¹ããŒã¿ã¹ïŒç¹ã«**SPD**ïŒã確èªããŠããããããšãã€ããããç»é²ããŸãããã</p>
{/* ããããå & ãã€ãããå */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="block text-slate-300 font-bold">æ©äœå</label>
<input
type="text"
value={robotName}
onChange={(e) => setRobotName(e.target.value)}
placeholder="ããããåãå
¥å"
className="w-full p-3 bg-slate-700 border border-slate-600 rounded-lg text-white focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
{/* ãã€ãããåå
¥å */}
<div className="space-y-2">
<label className="block text-slate-300 font-bold">ãã€ãããå</label>
<input
type="text"
value={pilotName}
onChange={(e) => setPilotName(e.target.value)}
placeholder="ãã€ãããåãå
¥å"
className="w-full p-3 bg-slate-700 border border-slate-600 rounded-lg text-white focus:ring-yellow-500 focus:border-yellow-500"
/>
</div>
</div>
{/* ããŒãéžæ */}
<div className="grid grid-cols-2 gap-4">
{Object.keys(ROBOT_PARTS).map(partKey => (
<div key={partKey} className="space-y-1 p-3 bg-slate-800 rounded-lg">
<label className="block text-indigo-300 font-semibold capitalize">{partKey} ({selectedParts[partKey].icon})</label>
<select
value={selectedParts[partKey].name}
onChange={(e) => {
const newPart = ROBOT_PARTS[partKey].find(p => p.name === e.target.value);
setSelectedParts(prev => ({ ...prev, [partKey]: newPart }));
}}
className="w-full p-2 bg-slate-600 border border-slate-500 rounded-md text-white"
>
{ROBOT_PARTS[partKey].map(part => (
<option key={part.name} value={part.name}>
{part.name} (HP:{part.stats.hp || 0}, ATK:{part.stats.atk || 0}, DEF:{part.stats.def || 0}, SPD:{part.stats.spd || 0})
</option>
))}
</select>
</div>
))}
</div>
{/* ã«ã¹ã¿ã ã¡ãã»ãŒãžèšå® */}
<div className="bg-slate-700 p-4 rounded-xl shadow-inner space-y-3">
<h3 className="text-xl font-bold text-white border-b border-yellow-500 pb-2 flex items-center gap-2">
<MessageSquare size={20} className="text-yellow-400"/> ãã€ãããã»ãªãèšå®
</h3>
<p className="text-sm text-yellow-300">ð¡ è€æ°ã®ã»ãªãåè£ã <span className="font-extrabold text-lg bg-slate-800 p-1 rounded-md">â³</span> ã§åºåã£ãŠå
¥åãããšãæŠéæã«ã©ã³ãã ã«éžã°ããŸãã</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{Object.keys(DEFAULT_BATTLE_MESSAGES).map(key => (
<div key={key} className="space-y-1">
<label className="block text-sm text-slate-400 font-medium">{MESSAGE_LABELS[key]}</label>
<input
type="text"
value={customMessages[key]}
onChange={(e) => setCustomMessages(prev => ({ ...prev, [key]: e.target.value }))}
placeholder={DEFAULT_BATTLE_MESSAGES[key]}
maxLength={100}
className="w-full p-2 bg-slate-600 border border-slate-500 rounded-md text-white text-sm"
/>
</div>
))}
</div>
</div>
{/* ã¹ããŒã¿ã¹ãšä¿åãã¿ã³ */}
<div className="bg-slate-700 p-4 rounded-xl shadow-inner space-y-3">
<h3 className="text-xl font-bold text-white border-b border-indigo-500 pb-2">ç·åã¹ããŒã¿ã¹</h3>
<div className="flex justify-between">
<StatDisplay Icon={Heart} label="HP" value={calculateStats.hp} color="text-red-400"/>
<StatDisplay Icon={Zap} label="ATK" value={calculateStats.atk} color="text-yellow-400"/>
<StatDisplay Icon={Shield} label="DEF" value={calculateStats.def} color="text-cyan-400"/>
<StatDisplay Icon={FastForward} label="SPD" value={calculateStats.spd} color="text-green-400"/>
</div>
<button
onClick={handleSaveRobot}
disabled={loading || !userId}
className="w-full p-3 bg-indigo-600 hover:bg-indigo-700 rounded-lg font-bold flex items-center justify-center gap-2 transition disabled:bg-indigo-900/50 mt-4"
>
{loading ? <Loader2 size={20} className="animate-spin" /> : <Save size={20} />}
ãããããä¿å
</button>
</div>
{/* èªåã®ãããããªã¹ã */}
<h3 className="text-2xl font-bold text-slate-300 mt-6 border-b border-slate-600 pb-2">ããªãã®ç»é²ãããã ({myRobots.length})</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{myRobots.map(robot => (
<RobotCard
key={robot.id}
robot={robot}
type="my"
onClick={() => {
setRobotName(robot.name);
setPilotName(robot.pilotName || 'åç¡ãã®ãã€ããã');
// æ¢åã®ããŒã¿ãå
ã«ãROBOT_PARTSãã察å¿ããããŒããèŠã€ãã
setSelectedParts({
// parts.headãªã©ã空ãªããžã§ã¯ãã§ãã£ãŠããnameã¢ã¯ã»ã¹ã¯å®å
šã«ãªã
head: ROBOT_PARTS.head.find(p => p.name === (robot.parts?.head?.name || ROBOT_PARTS.head[0].name)) || ROBOT_PARTS.head[0],
body: ROBOT_PARTS.body.find(p => p.name === (robot.parts?.body?.name || ROBOT_PARTS.body[0].name)) || ROBOT_PARTS.body[0],
arms: ROBOT_PARTS.arms.find(p => p.name === (robot.parts?.arms?.name || ROBOT_PARTS.arms[0].name)) || ROBOT_PARTS.arms[0],
legs: ROBOT_PARTS.legs.find(p => p.name === (robot.parts?.legs?.name || ROBOT_PARTS.legs[0].name)) || ROBOT_PARTS.legs[0],
});
setCustomMessages(robot.battleMessages || DEFAULT_BATTLE_MESSAGES);
}}
/>
))}
</div>
</div>
);
const renderOpponentSelectMode = () => {
const availableOpponents = opponentRobots.filter(bot =>
!selectedMyRobot || bot.id !== selectedMyRobot.id
);
return (
<div className="p-4 md:p-8 space-y-6 max-w-3xl mx-auto">
<h2 className="text-3xl font-extrabold text-red-400 flex items-center gap-2">
<Sword size={28} /> 察æŠçžæéžæ
</h2>
<p className="text-slate-400">察æŠçžæãããŒã¿ããŒã¹ããéžæããããªãã®ãããããšåè² ãããŸãããïŒ</p>
{/* èªåã®ããããéžæ */}
<div className="border-2 border-indigo-500 rounded-xl p-4 bg-slate-800">
<h3 className="text-xl font-bold text-indigo-300 mb-3">察æŠã«äœ¿çšããããªãã®ãããã</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{myRobots.length > 0 ? myRobots.map(robot => (
<RobotCard
key={robot.id}
robot={robot}
type="my"
isSelected={selectedMyRobot && selectedMyRobot.id === robot.id}
onClick={() => {
setSelectedMyRobot(robot);
setSelectedOpponent(null);
}}
/>
)) : (
<p className="col-span-3 text-slate-500">æ§ç¯ã¢ãŒãã§ãããããäœæã»ä¿åããŠãã ããã</p>
)}
</div>
</div>
{/* çžæãããããªã¹ã */}
<div className="border-2 border-red-500 rounded-xl p-4 bg-slate-800">
<h3 className="text-xl font-bold text-red-300 mb-3 flex justify-between items-center">
察æŠçžæãªã¹ã ({availableOpponents.length})
<button onClick={loadAllPublicRobots} disabled={loading} className="text-sm px-3 py-1 bg-red-600/50 hover:bg-red-600 rounded-full transition flex items-center gap-1">
{loading ? <Loader2 size={16} className="animate-spin" /> : 'æŽæ°'}
</button>
</h3>
{!selectedMyRobot && <p className="text-yellow-400 mb-3">å
ã«äžéšã®ã察æŠã«äœ¿çšããããªãã®ããããããéžæããŠãã ããã</p>}
{availableOpponents.length === 0 ? (
<p className="text-slate-500">察æŠçžæãããŸãããïŒèªåã®ãããããïŒäœã®ã¿ã®å Žåã¯ãæ§ç¯ã¢ãŒãã§ïŒäœç®ãäœæããŠãã ããïŒ</p>
) : (
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{availableOpponents.map(robot => (
<RobotCard
key={robot.id}
robot={robot}
type="opponent"
isSelected={selectedOpponent && selectedOpponent.id === robot.id}
onClick={() => setSelectedOpponent(robot)}
/>
))}
</div>
)}
</div>
{/* ããã«éå§ãã¿ã³ */}
<button
onClick={() => { setMode('BATTLE'); startBattle(); }}
disabled={!selectedMyRobot || !selectedOpponent || selectedMyRobot.id === selectedOpponent.id}
className="w-full p-4 text-xl font-extrabold bg-green-600 hover:bg-green-700 rounded-lg flex items-center justify-center gap-2 transition disabled:bg-green-900/50"
>
<Sword size={24}/> ããã«éå§ïŒ
</button>
</div>
);
};
const renderBattleMode = () => {
if (!selectedMyRobot || !selectedOpponent) {
// 察æŠçžæãéžæãããŠããªãå Žåã¯éžæç»é¢ã«æ»ã
setMode('SELECT_OPPONENT');
return (
<div className="p-10 text-center text-red-400 mt-10">
<h2 className="text-2xl font-bold">â ïž å¯ŸæŠãšã©ãŒ</h2>
<p className="mt-2">察æŠã«å¿
èŠãªããããããŒã¿ãèŠã€ãããŸããã§ããã察æŠçžæéžæç»é¢ã«æ»ããŸãã</p>
</div>
);
}
const isMyRobotWinner = !isSimulating && selectedMyRobot.id && battleResultId === selectedMyRobot.id;
const isOpponentWinner = !isSimulating && selectedOpponent.id && battleResultId === selectedOpponent.id;
return (
<div className="p-4 md:p-8 space-y-6 max-w-4xl mx-auto">
<h2 className="text-3xl font-extrabold text-green-400 flex items-center gap-2">
<Zap size={28} /> ããã«ã·ãã¥ã¬ãŒã·ã§ã³
</h2>
<p className="text-slate-400">ãã¡ãŒãžã«ä¹±æ°èŠçŽ ãå ãããŸãããåãçµã¿åããã§ãçµæãå€ããå¯èœæ§ããããŸãïŒ</p>
{/* ãããã察æŠè¡šç€º */}
<div className="flex justify-around items-start p-4 bg-slate-800 rounded-xl shadow-2xl">
<div className={`p-4 rounded-lg transition duration-500
${isMyRobotWinner ? 'ring-4 ring-indigo-400' : ''}`}>
<RobotVisualDisplay robot={selectedMyRobot} />
<p className="text-xl font-bold text-center text-indigo-300 mt-2">{selectedMyRobot.name}</p>
<p className="text-sm font-semibold text-center text-yellow-300">({selectedMyRobot.pilotName || 'åç¡ãã®ãã€ããã'})</p>
</div>
<div className="text-4xl font-black text-red-500 p-4">VS</div>
<div className={`p-4 rounded-lg transition duration-500
${isOpponentWinner ? 'ring-4 ring-red-400' : ''}`}>
<RobotVisualDisplay robot={selectedOpponent} />
<p className="text-xl font-bold text-center text-red-300 mt-2">{selectedOpponent.name}</p>
<p className="text-sm font-semibold text-center text-yellow-300">({selectedOpponent.pilotName || 'åç¡ãã®ãã€ããã'})</p>
</div>
</div>
{/* ããã«ãã° */}
<div className="bg-slate-700 h-80 overflow-y-auto p-4 rounded-xl font-mono text-sm space-y-1 shadow-inner">
<h3 className="text-lg font-bold text-white sticky top-0 bg-slate-700/90 py-1">æŠéãã°</h3>
{battleLog.map((entry, index) => {
if (!entry || typeof entry !== 'object') {
return null;
}
// ãã¬ãŒã·ã§ã³ã®ã¬ã³ããªã³ã°
if (entry.type === 'narration') {
return (
<p key={index}
className="text-yellow-400 font-medium italic bg-slate-600/50 p-1 rounded-md mb-1"
dangerouslySetInnerHTML={{ __html: `ð¢ ${entry.text}` }}
/>
);
}
// ã«ã¹ã¿ã ã¡ãã»ãŒãžã®ã¬ã³ããªã³ã°
if (entry.type === 'message') {
const speakerColor = entry.isA ? 'text-indigo-300' : 'text-red-300';
const textAlignment = entry.isA ? 'text-left' : 'text-right';
const bgColor = entry.isA ? 'bg-indigo-900/30' : 'bg-red-900/30';
const nameDisplay = entry.isA ? `[${entry.name}]` : `[${entry.name}]`;
return (
<p key={index} className={`font-bold italic ${speakerColor} ${textAlignment} px-2 py-1 rounded-md ${bgColor} animate-pulse-once`}>
{nameDisplay}: "{entry.text}"
</p>
);
}
if (entry.message) {
// ã·ã¹ãã ã¡ãã»ãŒãžã®è¡šç€º
return <p key={index} className="text-green-400 font-bold border-t border-slate-600 pt-1">{entry.message}</p>;
}
// ã¿ãŒã³ãã°ã®å Žå
if (entry.damage === undefined || entry.atk === undefined || entry.def === undefined) {
return null;
}
const isMyAttack = entry.isA;
const textColor = isMyAttack ? 'text-indigo-200' : 'text-red-200';
return (
<div key={index} className={`flex justify-between ${textColor} border-b border-slate-700/50 py-1`}>
<span>ã¹ããã {entry.step}: {entry.attacker} ã®æ»æ! </span>
<span className="text-xs text-slate-400"> (ATK:{entry.atk}/DEF:{entry.def})</span>
<span>{entry.defender} ã« <span className="text-lg font-extrabold text-yellow-300">{entry.damage}</span> ãã¡ãŒãž!</span>
<span className="text-slate-400"> (HP A:{entry.hpA}, B:{entry.hpB})</span>
</div>
);
})}
{isSimulating && <Loader2 size={24} className="animate-spin text-green-400 mt-4 mx-auto" />}
</div>
{/* çµæè¡šç€º */}
{battleResult && (
<div className="text-center p-4 bg-green-800/50 rounded-lg">
<p className="text-3xl font-extrabold text-green-300">
{battleResult === 'æéåã/åŒãåã' ? battleResult : `WINNER: ${battleResult}`}
</p>
</div>
)}
{/* æ»ããã¿ã³ */}
<button
onClick={() => { setMode('SELECT_OPPONENT'); setBattleResult(null); setBattleResultId(null); setBattleLog([]); loadAllPublicRobots(); }}
className="w-full p-3 bg-indigo-600 hover:bg-indigo-700 rounded-lg font-bold transition"
>
察æŠçžæéžæã«æ»ã
</button>
</div>
);
};
const RobotVisualDisplay = ({ robot }) => (
<div className="flex flex-col items-center justify-center p-4">
<div className={`text-4xl ${robot.parts?.head?.color || 'text-white'}`}>{robot.parts?.head?.icon || 'â'}</div>
<div className={`text-5xl ${robot.parts?.arms?.color || 'text-white'} flex items-center`}>
{robot.parts?.arms?.icon || 'â'}
<span className={`text-3xl mx-3 ${robot.parts?.body?.color || 'text-white'}`}>{robot.parts?.body?.icon || 'â'}</span>
{robot.parts?.arms?.icon || 'â'}
</div>
<div className={`text-4xl ${robot.parts?.legs?.color || 'text-white'}`}>{robot.parts?.legs?.icon || 'â'}</div>
<p className="text-sm text-slate-500 mt-2">
{robot.userId === userId ? 'ããªã' : `ID: ${robot.userId?.slice(0, 8)}...`}
</p>
</div>
);
// --- ã¡ã€ã³ã¬ã³ããªã³ã° (倿Žãªã) ---
if (loading && !isAuthReady) {
return (
<div className="flex flex-col items-center justify-center h-screen bg-slate-900 text-white">
<Loader2 size={48} className="animate-spin text-indigo-400" />
<p className="mt-4 text-xl">ã·ã¹ãã èµ·åäžïŒèªèšŒåŸ
æ©ïŒ...</p>
</div>
);
}
if (error) {
return (
<div className="flex flex-col items-center justify-center h-screen bg-slate-900 text-red-400 p-8">
<h1 className="text-2xl font-bold">èŽåœçãªãšã©ãŒãçºçããŸãã</h1>
<p className="mt-4 text-center text-lg">{error}</p>
<p className="mt-8 text-sm text-slate-400 border-t border-slate-600 pt-4">
äžèšã®ã¡ãã»ãŒãžããèµ·åæã«ãã©ãŠã¶ã®**ã³ã³ãœãŒã«**ã«è¡šç€ºããããšã©ãŒå
容ãšäžèŽããªãå Žåã<br/>
ãææ°ã§ãããã³ã³ãœãŒã«ã«è¡šç€ºããã**èµ€ããšã©ãŒã¡ãã»ãŒãž**ããã®ãŸãŸã³ããŒïŒããŒã¹ãã§ãç¥ãããã ããã<br/>
ãããåé¡è§£æ±ºã®éµã«ãªããŸãã
</p>
</div>
);
}
return (
<div className="min-h-screen bg-slate-900 text-white font-sans p-2">
<header className="flex justify-between items-center p-4 bg-slate-800 shadow-xl rounded-b-xl sticky top-0 z-10">
<h1 className="text-2xl font-black text-white flex items-center gap-2">
<Zap size={28} className="text-yellow-400"/> MECHA BUILDER
</h1>
<div className="flex items-center gap-4">
<p className="text-sm text-slate-400 flex items-center">
<User size={16} className="mr-1"/> ãŠãŒã¶ãŒID: {userId ? userId.slice(0, 8) + '...' : 'æªèªèšŒ'}
</p>
<button
onClick={() => setMode('BUILD')}
className={`p-2 rounded-full transition ${mode === 'BUILD' ? 'bg-indigo-600' : 'bg-slate-700 hover:bg-slate-600'}`}
title="ããããæ§ç¯"
>
<Settings size={20} />
</button>
<button
onClick={() => { setMode('SELECT_OPPONENT'); loadAllPublicRobots(); }}
disabled={myRobots.length === 0}
className={`p-2 rounded-full transition ${mode === 'SELECT_OPPONENT' ? 'bg-red-600' : 'bg-slate-700 hover:bg-slate-600'} disabled:opacity-50`}
title="察æŠçžæéžæ"
>
<Sword size={20} />
</button>
</div>
</header>
<main>
{mode === 'BUILD' && renderBuildMode()}
{mode === 'SELECT_OPPONENT' && renderOpponentSelectMode()}
{mode === 'BATTLE' && renderBattleMode()}
</main>
</div>
);
};
export default App;
- #çæAI
- #ChatGPT
- #ã²ãŒã
- #æ¥æ¬
- #åç»
- #人é
- #æªæ¥
- #AIãšãã£ãŠã¿ã
- #ç©èª
- #Gemini
- #ç¶ç¶
- #æ å ±
- #æµ·å€
- #ãã¡ã³ã¿ãžãŒ
- #圌女
- #è©äŸ¡
- #䟡å€
- #çºä¿¡
- #æä»£
- #ãªã¹ã¯
- #æè¡
- #OpenAI
- #ã«ãŒã«
- #æŠç¥
- #ã¢ãã«
- #ããœã³ã³
- #ç¶ç¶ã¯åãªã
- #èŠç¹
- #ãããã
- #çæ
- #å±é
- #é¢çœ
- #ããŒãããŒ
- #èšå®
- #èœå
- #VRChat
- #ã¹ããŒãªãŒ
- #AIå°èª¬
- #ãšãŒãžã§ã³ã
- #Copilot
- #grok
- #åéº
- #æ©èœ
- #TRPG
- #話é¡
- #ç¶æ³
- #ä¿¡çš
- #windows
- #ç¶ç¶å
- #AIãšã€ãã£ãŠã¿ã
- #å°å³
- #ãŠãŒã¶ãŒ
- #ãšã
- #çè«
- #ãã€ãã³ãŒãã£ã³ã°
- #æ°ä»
- #äžæ
- #linux
- #ã¢ãã¿ãŒ
- #ããã«
- #屿©ç®¡ç
- #ãªã³ã¯
- #æŠè¡
- #èŠå¶
- #ããã«
- #API
- #ãµã€ããŒãã³ã¯
- #泚æ
- #è·å¡
- #ããŒãããœã³ã³
- #åæ
- #é²è¡
- #ç¶ç¶ã®ã³ã
- #ã·ãŒã³
- #èŠçŽ
- #絵垫
- #AIããŒãããŒãšã®æ¥åžž
- #èŠå
- #å€å®
- #å°è©
- #ã€ã³ãã¯ã
- #ã¯ãŒã«ã
- #ç±ç
- #察æŠ
- #AIããŒãããŒçé
- #ããžã¥ã¢ã«
- #AI絵垫
- #ã¯ããŒã
- #ãã£ãã
- #å°çš
- #ææç©
- #ããŒã«
- #察æŠã²ãŒã
- #AIã³ã³ãããªã³
- #æå
- #Grok4
- #ã³ã³ã·ã¥ãŒããŒ
- #ã»ãŒãããŒã¿
- #æããã«
- #ãã£ãããã€ãã€