mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-11 18:53:06 +00:00
[MIRROR] Add a new fishing minigame to NTOS (#9205)
Co-authored-by: ShadowLarkens <shadowlarkens@gmail.com> Co-authored-by: CHOMPStation2 <chompsation2@gmail.com>
This commit is contained in:
@@ -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
|
||||||
360
tgui/packages/tgui/interfaces/FishingMinigame.tsx
Normal file
360
tgui/packages/tgui/interfaces/FishingMinigame.tsx
Normal file
@@ -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 (
|
||||||
|
<Window width={200} height={530}>
|
||||||
|
<Window.Content>
|
||||||
|
<GameWindow />
|
||||||
|
</Window.Content>
|
||||||
|
</Window>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 ? (
|
||||||
|
<Stack align="center" justify="center" fill textAlign="center" fontSize={3}>
|
||||||
|
<Stack.Item>
|
||||||
|
<Box>
|
||||||
|
{gameOver === GameOverState.GameFail
|
||||||
|
? 'You Lost!'
|
||||||
|
: gameOver === GameOverState.GameWin
|
||||||
|
? 'You won!!!'
|
||||||
|
: 'Fishy Fishy 905'}
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setScore((MAX_SCORE + MIN_SCORE) / 2);
|
||||||
|
setGameOver(GameOverState.GameRunning);
|
||||||
|
}}
|
||||||
|
fluid
|
||||||
|
mt={1}
|
||||||
|
>
|
||||||
|
{gameOver === GameOverState.GameTitle ? 'Start?' : 'Restart?'}
|
||||||
|
</Button>
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
) : (
|
||||||
|
<Stack fill align="center" justify="center">
|
||||||
|
<Stack.Item position="relative" mt={-4}>
|
||||||
|
<Icon
|
||||||
|
name="fish"
|
||||||
|
spin
|
||||||
|
color="red"
|
||||||
|
size={2}
|
||||||
|
position="absolute"
|
||||||
|
top={-14}
|
||||||
|
left={-14}
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
name="fish"
|
||||||
|
spin
|
||||||
|
color="blue"
|
||||||
|
size={2}
|
||||||
|
position="absolute"
|
||||||
|
top={6}
|
||||||
|
left={-13}
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
name="fish"
|
||||||
|
spin
|
||||||
|
color="darkblue"
|
||||||
|
size={2}
|
||||||
|
position="absolute"
|
||||||
|
top={-3}
|
||||||
|
left={-3}
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
name="fish"
|
||||||
|
spin
|
||||||
|
color="pink"
|
||||||
|
size={2}
|
||||||
|
position="absolute"
|
||||||
|
top={10}
|
||||||
|
left={10}
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
name="fish"
|
||||||
|
spin
|
||||||
|
color="yellow"
|
||||||
|
size={2}
|
||||||
|
position="absolute"
|
||||||
|
top={-10}
|
||||||
|
left={11}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
italic
|
||||||
|
position="absolute"
|
||||||
|
left={-13}
|
||||||
|
top={2}
|
||||||
|
style={{ transform: 'rotate(34deg)' }}
|
||||||
|
>
|
||||||
|
Fish...
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
italic
|
||||||
|
position="absolute"
|
||||||
|
left={10}
|
||||||
|
top={1}
|
||||||
|
style={{ transform: 'rotate(-53deg)' }}
|
||||||
|
>
|
||||||
|
Fish!
|
||||||
|
</Box>
|
||||||
|
<FishingRod position="absolute" top={-8} left={6} />
|
||||||
|
<Box position="absolute" top={-18}>
|
||||||
|
<Bar setScore={setScore} />
|
||||||
|
</Box>
|
||||||
|
<Box position="absolute" left={-12} top={0}>
|
||||||
|
<ProgressBar
|
||||||
|
className="FishingGame__ProgressBar"
|
||||||
|
value={score}
|
||||||
|
minValue={MIN_SCORE}
|
||||||
|
maxValue={MAX_SCORE}
|
||||||
|
width={HEIGHT + 7}
|
||||||
|
style={{
|
||||||
|
transform: 'rotate(-90deg)',
|
||||||
|
}}
|
||||||
|
ranges={{
|
||||||
|
bad: [MIN_SCORE, MIN_SCORE / 2],
|
||||||
|
average: [MIN_SCORE / 2, MAX_SCORE / 2],
|
||||||
|
good: [MAX_SCORE / 2, MAX_SCORE],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
|
||||||
|
</ProgressBar>
|
||||||
|
</Box>
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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<React.SetStateAction<number>>;
|
||||||
|
}) => {
|
||||||
|
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 (
|
||||||
|
<MouseTracker
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
mouseDown = true;
|
||||||
|
}}
|
||||||
|
onMouseUp={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
mouseDown = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
position="relative"
|
||||||
|
width={2.5}
|
||||||
|
height={HEIGHT + 0.25 + DIFFICULTY}
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(#aacbf4, #4578e1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
top={barTopR}
|
||||||
|
left={0.25}
|
||||||
|
backgroundColor="#7fcd00"
|
||||||
|
style={{
|
||||||
|
border: '4px inset #6bb300',
|
||||||
|
}}
|
||||||
|
width={2}
|
||||||
|
height={DIFFICULTY}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
top={fishTopR}
|
||||||
|
left={0.1}
|
||||||
|
width={2}
|
||||||
|
height={2}
|
||||||
|
style={{ userSelect: 'none' }}
|
||||||
|
>
|
||||||
|
<Icon name="fish" size={2} color="darkblue" />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</MouseTracker>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type MouseTrackerProps = PropsWithChildren<{
|
||||||
|
onMouseDown?: (e: MouseEvent) => void;
|
||||||
|
onMouseUp?: (e: MouseEvent) => void;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export class MouseTracker extends Component<MouseTrackerProps> {
|
||||||
|
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 <span>{this.props.children}</span>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const FishingRod = (props: BoxProps) => {
|
||||||
|
return (
|
||||||
|
<Box {...props}>
|
||||||
|
<svg viewBox="0 0 256 256">
|
||||||
|
<g transform="rotate(-135 50 100) scale(1 -1)">
|
||||||
|
<path
|
||||||
|
fill="#626375"
|
||||||
|
d="M137.561,331.913L11.774,457.7c-12.422,12.422-12.422,32.561,0,44.984l0,0
|
||||||
|
c12.422,12.422,32.561,12.422,44.984,0l125.786-125.786L137.561,331.913z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#75778C"
|
||||||
|
d="M22.969,468.895l125.786-125.786l33.788,33.788l0,0l-44.984-44.984L11.774,457.7
|
||||||
|
c-12.422,12.421-12.422,32.561,0,44.984l0,0c1.932,1.932,4.053,3.563,6.301,4.895C10.849,495.377,12.481,479.385,22.969,468.895z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#C89173"
|
||||||
|
d="M497.668,15.644L486.462,4.438c-5.917-5.917-15.509-5.917-21.424,0L121.728,347.746l32.631,32.631
|
||||||
|
L497.668,37.069C503.585,31.152,503.585,21.56,497.668,15.644z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#E0A381"
|
||||||
|
d="M476.232,15.632c5.916-5.916,15.508-5.916,21.423,0L486.462,4.437
|
||||||
|
c-5.917-5.916-15.508-5.916-21.424,0L121.728,347.746l11.195,11.195L476.232,15.632z"
|
||||||
|
/>
|
||||||
|
<circle fill="#C9CFF2" cx="188.08" cy="394.532" r="50.535" />
|
||||||
|
<path
|
||||||
|
fill="#E6E9FF"
|
||||||
|
d="M163.544,369.988c17.832-17.832,45.672-19.548,65.437-5.158c-1.537-2.111-3.253-4.132-5.158-6.038
|
||||||
|
c-19.737-19.737-51.737-19.737-71.474,0c-19.737,19.737-19.737,51.737,0,71.474c1.904,1.904,3.926,3.621,6.038,5.158
|
||||||
|
C143.996,415.66,145.712,387.82,163.544,369.988z"
|
||||||
|
/>
|
||||||
|
<circle fill="#626375" cx="188.08" cy="394.532" r="23.179" />
|
||||||
|
<path
|
||||||
|
fill="#4D4E5C"
|
||||||
|
d="M232.276,488.79h-44.19c-4.637,0-8.396-3.759-8.396-8.396V394.53c0-4.637,3.759-8.396,8.396-8.396
|
||||||
|
s8.396,3.759,8.396,8.396v77.467h35.793c4.637,0,8.396,3.759,8.396,8.396C240.672,485.031,236.913,488.79,232.276,488.79z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#626375"
|
||||||
|
d="M226.118,330.206c-6.169,0-11.542-1.828-15.651-5.937c-3.279-3.279-3.279-8.596,0-11.874
|
||||||
|
c3.279-3.279,8.596-3.279,11.874,0c3.692,3.69,26.277-1.584,53.287-28.594c27.011-27.012,32.285-49.595,28.594-53.287
|
||||||
|
c-1.639-1.639-2.46-3.788-2.46-5.937s0.819-4.298,2.46-5.937c3.279-3.279,8.596-3.279,11.874,0c1.415,1.413,7.2,1.9,17.348-2.553
|
||||||
|
c11.374-4.991,24.138-14.239,35.939-26.04c27.012-27.011,32.287-49.595,28.594-53.287c-1.639-1.639-2.458-3.788-2.458-5.937
|
||||||
|
c0-2.148,0.819-4.298,2.46-5.937c3.279-3.28,8.596-3.279,11.874,0c1.412,1.414,7.199,1.901,17.347-2.553
|
||||||
|
c11.374-4.991,24.138-14.239,35.939-26.04c27.012-27.011,32.287-49.595,28.594-53.287c-3.279-3.279-3.279-8.596,0.001-11.874
|
||||||
|
c3.279-3.279,8.596-3.279,11.874,0c15.901,15.902-2.35,50.791-28.594,77.036c-13.242,13.242-27.825,23.734-41.066,29.544
|
||||||
|
c-4.689,2.057-11.417,4.431-18.221,4.86c-1.081,17.491-15.738,40.622-34.467,59.35c-13.242,13.242-27.826,23.734-41.066,29.544
|
||||||
|
c-4.689,2.057-11.417,4.431-18.223,4.86c-1.08,17.491-15.737,40.621-34.467,59.35C268.04,315.139,243.819,330.206,226.118,330.206z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
18
tgui/packages/tgui/interfaces/NtosFishing.tsx
Normal file
18
tgui/packages/tgui/interfaces/NtosFishing.tsx
Normal file
@@ -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<Data>();
|
||||||
|
return (
|
||||||
|
<NtosWindow theme={data.PC_device_theme} width={340} height={530}>
|
||||||
|
<NtosWindow.Content>
|
||||||
|
<FishingGame onLose={() => act('lose')} onWin={() => act('win')} />
|
||||||
|
</NtosWindow.Content>
|
||||||
|
</NtosWindow>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.FishingGame__ProgressBar > div {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
@@ -54,6 +54,7 @@
|
|||||||
@include meta.load-css('./interfaces/Changelog.scss');
|
@include meta.load-css('./interfaces/Changelog.scss');
|
||||||
@include meta.load-css('./interfaces/CrewManifest.scss');
|
@include meta.load-css('./interfaces/CrewManifest.scss');
|
||||||
@include meta.load-css('./interfaces/ExperimentConfigure.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/NuclearBomb.scss');
|
||||||
@include meta.load-css('./interfaces/Paper.scss');
|
@include meta.load-css('./interfaces/Paper.scss');
|
||||||
@include meta.load-css('./interfaces/Pda.scss');
|
@include meta.load-css('./interfaces/Pda.scss');
|
||||||
|
|||||||
@@ -3705,6 +3705,7 @@
|
|||||||
#include "code\modules\modular_computers\file_system\programs\generic\configurator.dm"
|
#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\email_client.dm"
|
||||||
#include "code\modules\modular_computers\file_system\programs\generic\file_browser.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\game.dm"
|
||||||
#include "code\modules\modular_computers\file_system\programs\generic\news_browser.dm"
|
#include "code\modules\modular_computers\file_system\programs\generic\news_browser.dm"
|
||||||
#include "code\modules\modular_computers\file_system\programs\generic\ntdownloader.dm"
|
#include "code\modules\modular_computers\file_system\programs\generic\ntdownloader.dm"
|
||||||
|
|||||||
Reference in New Issue
Block a user