diff --git a/index.html b/index.html index 461b0b6..45e49a7 100644 --- a/index.html +++ b/index.html @@ -1,81 +1,99 @@ + + + + + Liar, Cheater, Fired | Office Chaos Browser Game + + + + + + - - - - - Liar, Cheater, Fired | Office Chaos Browser Game - - - - - - + + + + + + + - - - - - - - + + + + - - - - - - - - - -
- - + + + +
+ + diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000..3f9aa8c Binary files /dev/null and b/public/favicon.png differ diff --git a/src/components/game/BossBaby.tsx b/src/components/game/BossBaby.tsx index 99a8d6c..e4405fc 100644 --- a/src/components/game/BossBaby.tsx +++ b/src/components/game/BossBaby.tsx @@ -50,7 +50,7 @@ export const BossBaby: React.FC = ({ animation: "bossEnter 0.4s ease-out", }} > -
+
{/* XP Title Bar */}
diff --git a/src/components/game/DesktopIcons.tsx b/src/components/game/DesktopIcons.tsx index 4028b89..cfbc793 100644 --- a/src/components/game/DesktopIcons.tsx +++ b/src/components/game/DesktopIcons.tsx @@ -21,26 +21,30 @@ const icons = [ { icon: , label: "Standup.exe", bg: "#dc2626" }, ]; -export const DesktopIcons: React.FC = () => ( -
- {icons.map((item) => ( -
+export const DesktopIcons: React.FC = () => { + const isMobile = typeof window !== "undefined" && window.innerWidth < 768; + if (isMobile) return null; + return ( +
+ {icons.map((item) => (
- {item.icon} +
+ {item.icon} +
+ + {item.label} +
- - {item.label} - -
- ))} -
-); + ))} +
+ ); +}; diff --git a/src/components/game/DraggableWindow.tsx b/src/components/game/DraggableWindow.tsx index 6fd0a18..1687c2d 100644 --- a/src/components/game/DraggableWindow.tsx +++ b/src/components/game/DraggableWindow.tsx @@ -28,19 +28,30 @@ export const DraggableWindow: React.FC = ({ closable = true, bodyStyle, }) => { + const isMobile = typeof window !== "undefined" && window.innerWidth < 768; + const effectiveWidth = isMobile + ? Math.min(width, window.innerWidth - 16) + : width; + const [pos, setPos] = useState({ - x: - initialX ?? - Math.max(50, (window.innerWidth - width) / 2 + Math.random() * 60 - 30), - y: - initialY ?? - Math.max(30, window.innerHeight / 2 - 200 + Math.random() * 60 - 30), + x: isMobile + ? Math.max(8, (window.innerWidth - effectiveWidth) / 2) + : (initialX ?? + Math.max( + 50, + (window.innerWidth - width) / 2 + Math.random() * 60 - 30, + )), + y: isMobile + ? 155 + : (initialY ?? + Math.max(30, window.innerHeight / 2 - 200 + Math.random() * 60 - 30)), }); const dragging = useRef(false); const offset = useRef({ x: 0, y: 0 }); const onMouseDown = useCallback( (e: React.MouseEvent) => { + if (isMobile) return; dragging.current = true; offset.current = { x: e.clientX - pos.x, y: e.clientY - pos.y }; @@ -60,18 +71,18 @@ export const DraggableWindow: React.FC = ({ window.addEventListener("mousemove", onMove); window.addEventListener("mouseup", onUp); }, - [pos], + [pos, isMobile], ); return (
{icon} diff --git a/src/components/game/FailMeter.tsx b/src/components/game/FailMeter.tsx index bb8a821..8074ab8 100644 --- a/src/components/game/FailMeter.tsx +++ b/src/components/game/FailMeter.tsx @@ -16,7 +16,11 @@ export const FailMeter: React.FC = ({ value }) => { const needleY = cy - r * 0.82 * Math.sin(radians); const status = - value <= -STAGE_METER_POINT_CUTOF ? "CRITICAL" : value <= STAGE_METER_POINT_CUTOF ? "NEUTRAL" : "EXCELLENT"; + value <= -STAGE_METER_POINT_CUTOF + ? "CRITICAL" + : value <= STAGE_METER_POINT_CUTOF + ? "NEUTRAL" + : "EXCELLENT"; const statusColor = value <= -STAGE_METER_POINT_CUTOF diff --git a/src/components/game/IntroScreen.tsx b/src/components/game/IntroScreen.tsx index 07cfcf5..ce1e2b4 100644 --- a/src/components/game/IntroScreen.tsx +++ b/src/components/game/IntroScreen.tsx @@ -17,12 +17,26 @@ export const IntroScreen: React.FC = ({ setVolume, }) => { const [settingsOpen, setSettingsOpen] = useState(false); + const isMobile = typeof window !== "undefined" && window.innerWidth < 768; + const isLandscape = + typeof window !== "undefined" && window.innerWidth > window.innerHeight; + const isMobileLandscape = isMobile && isLandscape; + const settingsPanelWidth = isMobile + ? Math.max(180, window.innerWidth - 80) + : 320; return ( -
+
-
+
@@ -38,15 +52,18 @@ export const IntroScreen: React.FC = ({
-
+
-

+

LIAR...

-

+

CHEATER...

-

+

FIRED.

@@ -57,25 +74,25 @@ export const IntroScreen: React.FC = ({ BOSS MESSAGE
-
-
-
-

+

+
+
+

"I hate liars and cheaters. Why are you hired if you don't do your job?{" "} Focus on work!"

-
-
+
+
Boss Baby = ({
-
+
= ({ "hsl(0,0%,100%) hsl(220,10%,55%) hsl(220,10%,55%) hsl(0,0%,100%)", }} > -
+
(Disclaimer: This is not representative of us developers; we are very, very, very good employees.)
@@ -114,13 +131,13 @@ export const IntroScreen: React.FC = ({
= ({ onWin, onLose }) => { + const isMobile = typeof window !== "undefined" && window.innerWidth < 768; + const isLandscape = + typeof window !== "undefined" && window.innerWidth > window.innerHeight; + + // More aggressive scaling for landscape mobile + let canvasScale = 1; + if (isMobile && isLandscape) { + canvasScale = Math.min(1, (window.innerHeight - 120) / CANVAS_H); + } else if (isMobile) { + canvasScale = Math.min(1, (window.innerWidth - 48) / CANVAS_W); + } + const scaledCanvasWidth = CANVAS_W * canvasScale; + const scaledCanvasHeight = CANVAS_H * canvasScale; + const canvasRef = useRef(null); const [emailsLeft, setEmailsLeft] = useState(EMAIL_COUNT); const stateRef = useRef(null); + const setNextDir = useCallback((dx: number, dy: number) => { + if (stateRef.current) stateRef.current.nextDir = { dx, dy }; + }, []); + const initState = useCallback(() => { const paths: { x: number; y: number }[] = []; for (let r = 0; r < ROWS; r++) @@ -385,17 +405,35 @@ export const PacmanGame: React.FC = ({ onWin, onLose }) => { }, [initState, onWin, onLose]); return ( -
+
{/* Info / Emails */}
Emails: {emailsLeft} @@ -403,12 +441,86 @@ export const PacmanGame: React.FC = ({ onWin, onLose }) => {
{/* Canvas */} - +
+ +
+ + {/* Mobile D-pad */} + {isMobile && ( +
+ +
+ + +
+ +
+ )} {/* Status Bar */}
= ({ onWin, onLose }) => { background: "#000080", color: "#fff", padding: "2px 6px", - fontSize: 12, + fontSize: 11, + width: "100%", + boxSizing: "border-box", + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", }} > Avoid the 6 coworkers! diff --git a/src/components/game/PingPongGame.tsx b/src/components/game/PingPongGame.tsx index bde348c..465df5e 100644 --- a/src/components/game/PingPongGame.tsx +++ b/src/components/game/PingPongGame.tsx @@ -14,6 +14,13 @@ export const PingPongGame: React.FC = ({ playerAvatar, botAvatar, }) => { + const isMobile = typeof window !== "undefined" && window.innerWidth < 768; + const CANVAS_W = 350; + const CANVAS_H = 240; + const canvasScale = isMobile + ? Math.min(1, (window.innerWidth - 48) / CANVAS_W) + : 1; + // Physics Constants for perfectly consistent speed const CONSTANT_VX = 4.0; const MAX_VY = 3.5; @@ -37,6 +44,7 @@ export const PingPongGame: React.FC = ({ aiScore: number; running: boolean; keysDown: Set; + framesSinceScore: number; }>({ ballX: 175, ballY: 120, @@ -48,6 +56,7 @@ export const PingPongGame: React.FC = ({ aiScore: 0, running: true, keysDown: new Set(), + framesSinceScore: 0, }); const [scores, setScores] = useState({ player: 0, ai: 0 }); @@ -68,6 +77,20 @@ export const PingPongGame: React.FC = ({ window.addEventListener("keydown", keyDown); window.addEventListener("keyup", keyUp); + // Touch controls – drag anywhere on canvas to control paddle + const touchHandler = (e: TouchEvent) => { + e.preventDefault(); + const touch = e.touches[0]; + if (!touch) return; + const rect = canvas.getBoundingClientRect(); + const relY = (touch.clientY - rect.top) * (CANVAS_H / rect.height); + g.paddleY = Math.max(0, Math.min(CANVAS_H - 60, relY - 30)); + }; + canvas.addEventListener("touchmove", touchHandler, { passive: false }); + canvas.addEventListener("touchstart", touchHandler as EventListener, { + passive: true, + }); + const PADDLE_SPEED = 5; const AI_PADDLE_SPEED = 2.2; // Optimized for consistent ball speed const PADDLE_H = 60; @@ -75,6 +98,30 @@ export const PingPongGame: React.FC = ({ const loop = () => { if (!g.running) return; + // Increment frames counter + g.framesSinceScore++; + + // If rally lasts too long, gradually speed up the ball (linear) + const RALLY_TIMEOUT_FRAMES = 300; // ~5 seconds at 60fps + if (g.framesSinceScore > RALLY_TIMEOUT_FRAMES) { + const excessFrames = g.framesSinceScore - RALLY_TIMEOUT_FRAMES; + const boost = excessFrames * 0.02; // linear: add 0.02 pixels/frame per excess frame + + // Calculate base speed magnitude + const baseMagnitude = Math.sqrt( + CONSTANT_VX * CONSTANT_VX + BALL_SPEED_Y_START * BALL_SPEED_Y_START, + ); + const currentMagnitude = Math.sqrt( + g.ballVX * g.ballVX + g.ballVY * g.ballVY, + ); + const targetMagnitude = baseMagnitude + boost; + + // Scale velocity to new magnitude while preserving direction + const scale = targetMagnitude / currentMagnitude; + g.ballVX *= scale; + g.ballVY *= scale; + } + // Keyboard paddle control (W/S or Up/Down) if ( g.keysDown.has("w") || @@ -139,6 +186,7 @@ export const PingPongGame: React.FC = ({ g.ballY = 120; g.ballVX = CONSTANT_VX; g.ballVY = BALL_SPEED_Y_START; + g.framesSinceScore = 0; } if (g.ballX > 350) { g.playerScore++; @@ -152,6 +200,7 @@ export const PingPongGame: React.FC = ({ g.ballY = 120; g.ballVX = -CONSTANT_VX; g.ballVY = BALL_SPEED_Y_START; + g.framesSinceScore = 0; } // ── DRAWING ── @@ -245,6 +294,8 @@ export const PingPongGame: React.FC = ({ g.running = false; window.removeEventListener("keydown", keyDown); window.removeEventListener("keyup", keyUp); + canvas.removeEventListener("touchmove", touchHandler); + canvas.removeEventListener("touchstart", touchHandler as EventListener); }; }, [onWin, onLose]); @@ -272,20 +323,34 @@ export const PingPongGame: React.FC = ({

- W/S or ↑/↓ to move. First to 3 wins! + {isMobile + ? "Touch the canvas to move your paddle." + : "W/S or ↑/↓ to move."}{" "} + First to 3 wins!

- + > + +
YOU: {scores.player} | MARTY SUPREME: {scores.ai} diff --git a/src/components/game/ProcrastinationDesktop.tsx b/src/components/game/ProcrastinationDesktop.tsx index c1816a6..97e2fab 100644 --- a/src/components/game/ProcrastinationDesktop.tsx +++ b/src/components/game/ProcrastinationDesktop.tsx @@ -52,7 +52,11 @@ export const ProcrastinationDesktop: React.FC = ({ hidden = false, disabled = false, }) => { - const [pos, setPos] = useState({ x: 96, y: 40 }); + const isMobile = typeof window !== "undefined" && window.innerWidth < 768; + const [pos, setPos] = useState({ + x: isMobile ? 8 : 96, + y: isMobile ? 155 : 40, + }); const [activeTab, setActiveTab] = useState<"cricket" | "cat" | "youtube">( "cricket", ); @@ -71,7 +75,8 @@ export const ProcrastinationDesktop: React.FC = ({ const currentInnings = prev[teamKey]; const australiaChasedTarget = - prev.battingTeam === "AUSTRALIA" && prev.australia.runs > prev.india.runs; + prev.battingTeam === "AUSTRALIA" && + prev.australia.runs > prev.india.runs; if (australiaChasedTarget) { return { ...prev, battingTeam: "DONE" }; @@ -136,22 +141,25 @@ export const ProcrastinationDesktop: React.FC = ({ const nextState: MatchState = teamKey === "india" ? { - ...prev, - india: nextInnings, - } + ...prev, + india: nextInnings, + } : { - ...prev, - australia: nextInnings, - }; + ...prev, + australia: nextInnings, + }; const ballPrefix = - prev.battingTeam === "INDIA" ? `IND ${formatOvers(nextInnings.balls)}` : `AUS ${formatOvers(nextInnings.balls)}`; + prev.battingTeam === "INDIA" + ? `IND ${formatOvers(nextInnings.balls)}` + : `AUS ${formatOvers(nextInnings.balls)}`; nextState.recentBalls = [...prev.recentBalls.slice(-11), ballSymbol]; nextState.lastBallCommentary = `${ballPrefix}: ${ballCommentary}`; const chaseCompleted = - prev.battingTeam === "AUSTRALIA" && nextState.australia.runs > nextState.india.runs; + prev.battingTeam === "AUSTRALIA" && + nextState.australia.runs > nextState.india.runs; if (chaseCompleted) { return { @@ -188,7 +196,10 @@ export const ProcrastinationDesktop: React.FC = ({ const australiaRuns = match.australia.runs; const target = indiaRuns + 1; const runsNeeded = Math.max(0, target - australiaRuns); - const ballsRemaining = Math.max(0, MAX_BALLS_PER_INNINGS - match.australia.balls); + const ballsRemaining = Math.max( + 0, + MAX_BALLS_PER_INNINGS - match.australia.balls, + ); const requiredRate = match.battingTeam === "AUSTRALIA" && ballsRemaining > 0 && runsNeeded > 0 ? ((runsNeeded * 6) / ballsRemaining).toFixed(2) @@ -269,9 +280,10 @@ export const ProcrastinationDesktop: React.FC = ({
{/* Title Bar — drag handle */} @@ -314,30 +326,33 @@ export const ProcrastinationDesktop: React.FC = ({ @@ -393,14 +408,18 @@ export const ProcrastinationDesktop: React.FC = ({

Last Over Feed

- {match.recentBalls.length > 0 ? match.recentBalls.join(" ") : "No balls yet"} + {match.recentBalls.length > 0 + ? match.recentBalls.join(" ") + : "No balls yet"}

{liveLine}

-

{match.lastBallCommentary}

+

+ {match.lastBallCommentary} +

diff --git a/src/components/game/PunishmentScreen.tsx b/src/components/game/PunishmentScreen.tsx index 230cbc8..65cee57 100644 --- a/src/components/game/PunishmentScreen.tsx +++ b/src/components/game/PunishmentScreen.tsx @@ -125,16 +125,16 @@ export const PunishmentScreen: React.FC = ({ }, [onComplete, gameStage, punishmentType]); return ( -
-
-
+
+
+
{punishment.icon} {punishment.title}
{punishmentType === "email" ? ( diff --git a/src/components/game/Taskbar.tsx b/src/components/game/Taskbar.tsx index 1ab9206..6d03e35 100644 --- a/src/components/game/Taskbar.tsx +++ b/src/components/game/Taskbar.tsx @@ -35,11 +35,11 @@ export const Taskbar: React.FC = ({ volume, setVolume }) => {
-
+
{/* Quick launch icons (Win7 style) */} -
+
{[ { icon: , label: "Chrome" }, { icon: , label: "Teams" }, @@ -65,7 +65,9 @@ export const Taskbar: React.FC = ({ volume, setVolume }) => { min={0} max={100} value={Math.round(volume * 100)} - onChange={(event) => setVolume(Number(event.target.value) / 100)} + onChange={(event) => + setVolume(Number(event.target.value) / 100) + } className="w-24 accent-primary" /> diff --git a/src/components/game/TeamsNotif.tsx b/src/components/game/TeamsNotif.tsx index 88e91a1..63c19bc 100644 --- a/src/components/game/TeamsNotif.tsx +++ b/src/components/game/TeamsNotif.tsx @@ -14,7 +14,7 @@ export function TeamsNotif({ onDismiss, onJoin }: TeamsNotifProps) { className="fixed bottom-14 right-4 z-50" style={{ fontFamily: "Tahoma, 'MS Sans Serif', sans-serif" }} > -
+
{/* XP Title Bar */}
💬 Microsoft Teams diff --git a/src/components/game/TetrisGame.tsx b/src/components/game/TetrisGame.tsx index 5e9dd27..1b36a98 100644 --- a/src/components/game/TetrisGame.tsx +++ b/src/components/game/TetrisGame.tsx @@ -34,8 +34,12 @@ const SHAPES = [ [0, 1, 1], [1, 1, 1], ], // J: 3×3 chunky J + [[1, 1, 1, 1]], // I: 1×4 bar ]; +const CANVAS_W = COLS * CELL; +const CANVAS_H = ROWS * CELL; + const PRIORITIES = ["CRIT", "HIGH", "MED", "LOW"]; const COLORS = ["#ff3333", "#fb5607", "#ffbe0b", "#22c55e"]; const HIGHLIGHT_COLORS = ["#ff9999", "#fdb080", "#ffe580", "#86efac"]; @@ -50,10 +54,20 @@ export const TetrisGame: React.FC = ({ onTopReached, onCleared, }) => { + const isMobile = typeof window !== "undefined" && window.innerWidth < 768; + const canvasScale = isMobile + ? Math.min(1, (window.innerWidth - 48) / CANVAS_W) + : 1; + const canvasRef = useRef(null); const [timeLeft, setTimeLeft] = useState(TIMER_SECONDS); const [flashMsg, setFlashMsg] = useState(null); const flashTimeout = useRef | null>(null); + const ctrlRef = useRef<{ + move: (dir: number) => void; + rotate: () => void; + drop: () => void; + } | null>(null); useEffect(() => { const canvas = canvasRef.current; @@ -285,12 +299,14 @@ export const TetrisGame: React.FC = ({ }; window.addEventListener("keydown", keyHandler); + ctrlRef.current = { move, rotate, drop }; newPiece(); startTime = performance.now(); requestAnimationFrame(gameLoop); return () => { running = false; + ctrlRef.current = null; window.removeEventListener("keydown", keyHandler); if (flashTimeout.current) clearTimeout(flashTimeout.current); }; @@ -317,12 +333,24 @@ export const TetrisGame: React.FC = ({ {timeLeft}s
-
+
{flashMsg && (
@@ -332,9 +360,38 @@ export const TetrisGame: React.FC = ({
)}
-

- ↑ rotate  ·  ← → move  ·  ↓ drop -

+ {isMobile ? ( +
+ + + + +
+ ) : ( +

+ ↑ rotate  ·  ← → move  ·  ↓ drop +

+ )}
); }; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index e23a5d4..5e18409 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -24,6 +24,7 @@ const STAGE_METER_POINT_CUTOF = 50; export { STAGE_METER_POINT_CUTOF }; const Index = () => { + const isMobile = typeof window !== "undefined" && window.innerWidth < 768; const { state, setStage, moveMeter } = useGameState(); const [skipTutorials, setSkipTutorials] = useState(true); const [volume, setVolume] = useState(0.5); @@ -56,8 +57,7 @@ const Index = () => { const audio = bgAudioRef.current; if (!audio || !isAudioRunning) return; - void audio.play().catch(() => { - }); + void audio.play().catch(() => {}); }, [isAudioRunning]); useEffect(() => { @@ -482,7 +482,20 @@ const Index = () => { backgroundRepeat: "no-repeat", }} > -
+
@@ -718,10 +731,7 @@ const Index = () => { /> )} - +
); };