repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
Robin-hood Game
stars
latest
clone command
git clone gitlawb://did:key:z6MkwMR9...adEc/robin-hood-gamegit clone gitlawb://did:key:z6MkwMR9.../robin-hood-gamec8777c8csync from playground5h ago| #1 | import { useRef, useEffect, useState } from 'react'; |
| #2 | import { GAME_SIZE } from './game/constants'; |
| #3 | import type { GameSettings } from './game/types'; |
| #4 | import { loadSettings, saveSettings } from './game/storage'; |
| #5 | import { setMuted, playClick } from './game/sound'; |
| #6 | |
| #7 | interface SettingsProps { |
| #8 | onBack: () => void; |
| #9 | } |
| #10 | |
| #11 | export default function Settings({ onBack }: SettingsProps) { |
| #12 | const canvasRef = useRef<HTMLCanvasElement>(null); |
| #13 | const [settings, setSettings] = useState<GameSettings>(loadSettings); |
| #14 | |
| #15 | useEffect(() => { |
| #16 | const canvas = canvasRef.current; |
| #17 | if (!canvas) return; |
| #18 | const ctx = canvas.getContext('2d'); |
| #19 | if (!ctx) return; |
| #20 | |
| #21 | // Background |
| #22 | ctx.fillStyle = '#0d1a0d'; |
| #23 | ctx.fillRect(0, 0, GAME_SIZE, GAME_SIZE); |
| #24 | |
| #25 | // Title |
| #26 | ctx.fillStyle = '#DAA520'; |
| #27 | ctx.font = 'bold 24px monospace'; |
| #28 | ctx.textAlign = 'center'; |
| #29 | ctx.fillText('SETTINGS', GAME_SIZE / 2, 50); |
| #30 | |
| #31 | ctx.fillStyle = '#3a5a30'; |
| #32 | ctx.fillRect(60, 62, GAME_SIZE - 120, 2); |
| #33 | |
| #34 | // Sound toggle |
| #35 | const soundY = 110; |
| #36 | ctx.fillStyle = '#1a3a1a'; |
| #37 | ctx.fillRect(40, soundY, GAME_SIZE - 80, 44); |
| #38 | ctx.strokeStyle = '#3a5a30'; |
| #39 | ctx.lineWidth = 2; |
| #40 | ctx.strokeRect(40, soundY, GAME_SIZE - 80, 44); |
| #41 | ctx.fillStyle = '#ccc'; |
| #42 | ctx.font = 'bold 14px monospace'; |
| #43 | ctx.textAlign = 'left'; |
| #44 | ctx.fillText('Sound', 60, soundY + 27); |
| #45 | // Toggle indicator |
| #46 | ctx.fillStyle = settings.soundEnabled ? '#35a035' : '#555'; |
| #47 | ctx.fillRect(GAME_SIZE - 100, soundY + 12, 40, 20); |
| #48 | ctx.fillStyle = settings.soundEnabled ? '#fff' : '#888'; |
| #49 | ctx.fillRect( |
| #50 | settings.soundEnabled ? GAME_SIZE - 68 : GAME_SIZE - 98, |
| #51 | soundY + 14, |
| #52 | 16, 16 |
| #53 | ); |
| #54 | ctx.fillStyle = '#aaa'; |
| #55 | ctx.font = '11px monospace'; |
| #56 | ctx.textAlign = 'center'; |
| #57 | ctx.fillText(settings.soundEnabled ? 'ON' : 'OFF', GAME_SIZE - 80, soundY + 27); |
| #58 | |
| #59 | // Difficulty |
| #60 | const diffY = 180; |
| #61 | ctx.fillStyle = '#ccc'; |
| #62 | ctx.font = 'bold 14px monospace'; |
| #63 | ctx.textAlign = 'center'; |
| #64 | ctx.fillText('Difficulty', GAME_SIZE / 2, diffY); |
| #65 | |
| #66 | const difficulties: GameSettings['difficulty'][] = ['easy', 'normal', 'hard']; |
| #67 | const diffColors = ['#35a035', '#DAA520', '#ff3333']; |
| #68 | difficulties.forEach((diff, i) => { |
| #69 | const bx = 60 + i * 100; |
| #70 | const by = diffY + 15; |
| #71 | const isActive = settings.difficulty === diff; |
| #72 | ctx.fillStyle = isActive ? '#1a3a1a' : '#111'; |
| #73 | ctx.fillRect(bx, by, 80, 34); |
| #74 | ctx.strokeStyle = isActive ? diffColors[i] : '#333'; |
| #75 | ctx.lineWidth = 2; |
| #76 | ctx.strokeRect(bx, by, 80, 34); |
| #77 | ctx.fillStyle = isActive ? diffColors[i] : '#666'; |
| #78 | ctx.font = 'bold 12px monospace'; |
| #79 | ctx.textAlign = 'center'; |
| #80 | ctx.fillText(diff.toUpperCase(), bx + 40, by + 22); |
| #81 | }); |
| #82 | |
| #83 | // Back button |
| #84 | ctx.fillStyle = '#1a3a1a'; |
| #85 | ctx.fillRect(GAME_SIZE / 2 - 60, GAME_SIZE - 55, 120, 34); |
| #86 | ctx.strokeStyle = '#3a5a30'; |
| #87 | ctx.lineWidth = 2; |
| #88 | ctx.strokeRect(GAME_SIZE / 2 - 60, GAME_SIZE - 55, 120, 34); |
| #89 | ctx.fillStyle = '#ccc'; |
| #90 | ctx.font = 'bold 14px monospace'; |
| #91 | ctx.textAlign = 'center'; |
| #92 | ctx.fillText('Back', GAME_SIZE / 2, GAME_SIZE - 33); |
| #93 | }, [settings]); |
| #94 | |
| #95 | const handleClick = (e: React.PointerEvent) => { |
| #96 | const rect = (e.target as HTMLElement).getBoundingClientRect(); |
| #97 | const scaleX = GAME_SIZE / rect.width; |
| #98 | const scaleY = GAME_SIZE / rect.height; |
| #99 | const x = (e.clientX - rect.left) * scaleX; |
| #100 | const y = (e.clientY - rect.top) * scaleY; |
| #101 | |
| #102 | // Sound toggle |
| #103 | if (y >= 110 && y <= 154 && x >= 40 && x <= GAME_SIZE - 40) { |
| #104 | playClick(); |
| #105 | const next = { ...settings, soundEnabled: !settings.soundEnabled }; |
| #106 | setSettings(next); |
| #107 | saveSettings(next); |
| #108 | setMuted(!next.soundEnabled); |
| #109 | return; |
| #110 | } |
| #111 | |
| #112 | // Difficulty |
| #113 | if (y >= 195 && y <= 229) { |
| #114 | const difficulties: GameSettings['difficulty'][] = ['easy', 'normal', 'hard']; |
| #115 | difficulties.forEach((diff, i) => { |
| #116 | const bx = 60 + i * 100; |
| #117 | if (x >= bx && x <= bx + 80) { |
| #118 | playClick(); |
| #119 | const next = { ...settings, difficulty: diff }; |
| #120 | setSettings(next); |
| #121 | saveSettings(next); |
| #122 | } |
| #123 | }); |
| #124 | return; |
| #125 | } |
| #126 | |
| #127 | // Back |
| #128 | if (y >= GAME_SIZE - 55 && y <= GAME_SIZE - 21) { |
| #129 | playClick(); |
| #130 | onBack(); |
| #131 | } |
| #132 | }; |
| #133 | |
| #134 | return ( |
| #135 | <canvas |
| #136 | ref={canvasRef} |
| #137 | width={GAME_SIZE} |
| #138 | height={GAME_SIZE} |
| #139 | onPointerDown={handleClick} |
| #140 | style={{ |
| #141 | width: 'min(100vw, 100vh)', |
| #142 | height: 'min(100vw, 100vh)', |
| #143 | maxWidth: '100%', |
| #144 | maxHeight: '100%', |
| #145 | imageRendering: 'pixelated', |
| #146 | cursor: 'pointer', |
| #147 | touchAction: 'none', |
| #148 | }} |
| #149 | /> |
| #150 | ); |
| #151 | } |
| #152 |