diff --git a/code/modules/modular_computers/file_system/programs/generic/fishing.dm b/code/modules/modular_computers/file_system/programs/generic/fishing.dm new file mode 100644 index 0000000000..21a0ce203d --- /dev/null +++ b/code/modules/modular_computers/file_system/programs/generic/fishing.dm @@ -0,0 +1,26 @@ +/datum/computer_file/program/fishing + filename = "fishingminigame" + filedesc = "Fishy Fishy 905" + program_icon_state = "arcade" + extended_desc = "A little fishing minigame for when you're really bored." + size = 3 + requires_ntnet = FALSE + available_on_ntnet = TRUE + tgui_id = "NtosFishing" + usage_flags = PROGRAM_ALL + +/datum/computer_file/program/fishing/tgui_data(mob/user) + return get_header_data() + +/datum/computer_file/program/fishing/tgui_act(action, list/params) + . = ..() + if(.) + return + + switch(action) + if("lose") + playsound(computer, 'sound/arcade/lose.ogg', 50, TRUE, extrarange = -3, falloff = 0.1) + . = TRUE + if("win") + playsound(computer, 'sound/arcade/win.ogg', 50, TRUE, extrarange = -3, falloff = 0.1) + . = TRUE diff --git a/tgui/packages/tgui/interfaces/FishingMinigame.tsx b/tgui/packages/tgui/interfaces/FishingMinigame.tsx new file mode 100644 index 0000000000..3e8cf16fb8 --- /dev/null +++ b/tgui/packages/tgui/interfaces/FishingMinigame.tsx @@ -0,0 +1,360 @@ +import { clamp } from 'common/math'; +import { + Component, + PropsWithChildren, + ReactNode, + useEffect, + useState, +} from 'react'; +import { Box } from 'tgui/components'; +import { Window } from 'tgui/layouts'; +import { Button, Icon, ProgressBar, Stack } from 'tgui-core/components'; + +import { BoxProps } from '../components/Box'; + +enum GameOverState { + GameRunning = 0, + GameWin = 1, + GameFail = 2, + GameTitle = 3, +} + +export const FishingMinigame = (props) => { + return ( + + + + + + ); +}; + +export const GameWindow = (props: { + onWin?: () => void; + onLose?: () => void; +}) => { + const [gameOver, setGameOver] = useState(GameOverState.GameTitle); + const [score, setScore] = useState((MAX_SCORE + MIN_SCORE) / 2); + + useEffect(() => { + if (score === MIN_SCORE) { + if (props.onLose) { + props.onLose(); + } + setGameOver(GameOverState.GameFail); + } else if (score === MAX_SCORE) { + if (props.onWin) { + props.onWin(); + } + setGameOver(GameOverState.GameWin); + } + }, [score]); + + return gameOver ? ( + + + + {gameOver === GameOverState.GameFail + ? 'You Lost!' + : gameOver === GameOverState.GameWin + ? 'You won!!!' + : 'Fishy Fishy 905'} + + + + + ) : ( + + + + + + + + + Fish... + + + Fish! + + + + + + + +   + + + + + ); +}; + +// Settings +const UPDATE_RATE = 20; // ms per update interval +const HEIGHT = 30; // total height +const DIFFICULTY = 8; // vertical size of bar +const FISH_SPASM_CHANCE = 5; // %chance of fish changing direction +const BAR_UP_SPEED = 0.4; // amount bar moves up per tick +const BAR_FALL_SPEED = 0.6; // amount bar moves down per tick +const MAX_SCORE = 200; +const MIN_SCORE = -200; +// Reactive escape hatch +let barTop = HEIGHT; +let fishTop = 0; +let fishDirection = 0.25; +let mouseDown = false; + +export const Bar = (props: { + setScore: React.Dispatch>; +}) => { + const { setScore } = props; + + // Used to bridge non-reactive values back into reactive ones + const [barTopR, setBarTopR] = useState(HEIGHT); + const [fishTopR, setFishTopR] = useState(0); + + useEffect(() => { + const interval = setInterval(() => { + // Move bar + if (mouseDown) { + barTop = clamp(barTop - BAR_UP_SPEED, 0.25, HEIGHT); + } else { + barTop = clamp(barTop + BAR_FALL_SPEED, 0.25, HEIGHT); + } + + // Move fish + if (Math.random() > 1 - FISH_SPASM_CHANCE / 100) { + fishDirection = -fishDirection; + } + + fishTop = clamp(fishTop + fishDirection, 0.5, HEIGHT + 4); + + // Detect fish being inside the bar + if (fishTop > barTop && fishTop < barTop + DIFFICULTY) { + setScore((x) => clamp(x + 1, MIN_SCORE, MAX_SCORE)); + } else { + setScore((x) => clamp(x - 1, MIN_SCORE, MAX_SCORE)); + } + + // Update visuals + setBarTopR(barTop); + setFishTopR(fishTop); + }, UPDATE_RATE); + + return () => clearInterval(interval); + }, []); + + return ( + { + e.preventDefault(); + mouseDown = true; + }} + onMouseUp={(e) => { + e.preventDefault(); + mouseDown = false; + }} + > + + + + + + + + ); +}; + +type MouseTrackerProps = PropsWithChildren<{ + onMouseDown?: (e: MouseEvent) => void; + onMouseUp?: (e: MouseEvent) => void; +}>; + +export class MouseTracker extends Component { + constructor(props: MouseTrackerProps) { + super(props); + } + + onMouseDown = (e) => { + if (this.props.onMouseDown) { + this.props.onMouseDown(e); + } + }; + + onMouseUp = (e) => { + if (this.props.onMouseUp) { + this.props.onMouseUp(e); + } + }; + + componentDidMount(): void { + document.addEventListener('mousedown', this.onMouseDown); + document.addEventListener('mouseup', this.onMouseUp); + } + + componentWillUnmount(): void { + document.removeEventListener('mousedown', this.onMouseDown); + document.removeEventListener('mouseup', this.onMouseUp); + } + + render(): ReactNode { + return {this.props.children}; + } +} + +const FishingRod = (props: BoxProps) => { + return ( + + + + + + + + + + + + + + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/NtosFishing.tsx b/tgui/packages/tgui/interfaces/NtosFishing.tsx new file mode 100644 index 0000000000..742fab70d7 --- /dev/null +++ b/tgui/packages/tgui/interfaces/NtosFishing.tsx @@ -0,0 +1,18 @@ +import { useBackend } from '../backend'; +import { NtosWindow } from '../layouts'; +import { GameWindow as FishingGame } from './FishingMinigame'; + +type Data = { + PC_device_theme: string; +}; + +export const NtosFishing = (props) => { + const { act, data } = useBackend(); + return ( + + + act('lose')} onWin={() => act('win')} /> + + + ); +}; diff --git a/tgui/packages/tgui/styles/interfaces/FishingMinigame.scss b/tgui/packages/tgui/styles/interfaces/FishingMinigame.scss new file mode 100644 index 0000000000..1c22304cd2 --- /dev/null +++ b/tgui/packages/tgui/styles/interfaces/FishingMinigame.scss @@ -0,0 +1,3 @@ +.FishingGame__ProgressBar > div { + transition: none; +} diff --git a/tgui/packages/tgui/styles/main.scss b/tgui/packages/tgui/styles/main.scss index effa918048..6c9626dd9c 100644 --- a/tgui/packages/tgui/styles/main.scss +++ b/tgui/packages/tgui/styles/main.scss @@ -53,6 +53,7 @@ @include meta.load-css('./interfaces/Changelog.scss'); @include meta.load-css('./interfaces/CrewManifest.scss'); @include meta.load-css('./interfaces/ExperimentConfigure.scss'); +@include meta.load-css('./interfaces/FishingMinigame.scss'); @include meta.load-css('./interfaces/NuclearBomb.scss'); @include meta.load-css('./interfaces/Paper.scss'); @include meta.load-css('./interfaces/Pda.scss'); diff --git a/vorestation.dme b/vorestation.dme index e47e1a0626..dc56ab780e 100644 --- a/vorestation.dme +++ b/vorestation.dme @@ -3427,6 +3427,7 @@ #include "code\modules\modular_computers\file_system\programs\generic\configurator.dm" #include "code\modules\modular_computers\file_system\programs\generic\email_client.dm" #include "code\modules\modular_computers\file_system\programs\generic\file_browser.dm" +#include "code\modules\modular_computers\file_system\programs\generic\fishing.dm" #include "code\modules\modular_computers\file_system\programs\generic\game.dm" #include "code\modules\modular_computers\file_system\programs\generic\news_browser.dm" #include "code\modules\modular_computers\file_system\programs\generic\ntdownloader.dm"