From: Daniel Karbach Date: Thu, 29 Feb 2024 14:47:14 +0000 (+0100) Subject: guessing game settings X-Git-Url: https://git.localhorst.tv/?a=commitdiff_plain;h=93f50820771a0333b169f76f74727239cf0cb286;p=alttp.git guessing game settings --- diff --git a/app/Http/Controllers/ChannelController.php b/app/Http/Controllers/ChannelController.php index f1f3cdb..cbd0110 100644 --- a/app/Http/Controllers/ChannelController.php +++ b/app/Http/Controllers/ChannelController.php @@ -110,4 +110,59 @@ class ChannelController extends Controller { return $channel->toJson(); } + public function deleteCommand(Channel $channel, $command) { + $this->authorize('editRestream', $channel); + if (isset($channel->chat_commands[$command])) { + $cmds = $channel->chat_commands; + unset($cmds[$command]); + $channel->chat_commands = $cmds; + $channel->save(); + } + return $channel->toJson(); + } + + public function saveCommand(Request $request, Channel $channel, $command) { + $this->authorize('editRestream', $channel); + + $validatedData = $request->validate([ + 'command' => 'string', + 'restrict' => 'string', + 'type' => 'string', + ]); + + $cmds = $channel->chat_commands; + $cmds[$command] = $validatedData; + $channel->chat_commands = $cmds; + $channel->save(); + + return $channel->toJson(); + } + + public function saveGuessingGame(Request $request, Channel $channel, $name) { + $this->authorize('editRestream', $channel); + + $validatedData = $request->validate([ + 'active_message' => 'string', + 'cancel_message' => 'string', + 'invalid_solution_message' => 'string', + 'no_winners_message' => 'string', + 'not_active_message' => 'string', + 'points_exact_first' => 'numeric|min:1|max:5', + 'points_exact_other' => 'numeric|min:0|max:5', + 'points_close_first' => 'numeric|min:0|max:5', + 'points_close_min' => 'integer|min:0', + 'points_close_other' => 'numeric|min:0|max:5', + 'start_message' => 'string', + 'stop_message' => 'string', + 'winners_message' => 'string', + ]); + + $settings = $channel->guessing_settings; + $settings[$name] = $validatedData; + $channel->guessing_settings = $settings; + $channel->save(); + + return $channel->toJson(); + } + } diff --git a/app/Models/Channel.php b/app/Models/Channel.php index 32ac875..738312d 100644 --- a/app/Models/Channel.php +++ b/app/Models/Channel.php @@ -35,6 +35,17 @@ class Channel extends Model $this->save(); } + public function getGuessingSetting($name, $default = null) { + if (empty($this->guessing_settings) || + empty($this->guessing_type) || + !array_key_exists($this->guessing_type, $this->guessing_settings) || + !array_key_exists($name, $this->guessing_settings[$this->guessing_type]) + ) { + return $default; + } + return $this->guessing_settings[$this->guessing_type][$name]; + } + public function solveGuessing($solution) { $start = $this->guessing_start; $end = is_null($this->guessing_end) ? now() : $this->guessing_end; @@ -96,7 +107,20 @@ class Channel extends Model } public function scoreGuessing($solution, $guess, $first) { - return 1; + if ($guess == $solution) { + if ($first) { + return $this->getGuessingSetting('points_exact_first', 1); + } + return $this->getGuessingSetting('points_exact_other', 1); + } + $distance = abs(intval($guess) - intval($solution)); + if ($distance <= $this->getGuessingSetting('points_close_max', 3)) { + if ($first) { + return $this->getGuessingSetting('points_close_first', 1); + } + return $this->getGuessingSetting('points_close_other', 1); + } + return 0; } public function isValidGuess($solution) { @@ -129,6 +153,7 @@ class Channel extends Model 'chat' => 'boolean', 'chat_commands' => 'array', 'chat_settings' => 'array', + 'guessing_settings' => 'array', 'guessing_start' => 'datetime', 'guessing_end' => 'datetime', 'languages' => 'array', diff --git a/app/TwitchBot/ChatCommand.php b/app/TwitchBot/ChatCommand.php index d03f782..8e6fbe1 100644 --- a/app/TwitchBot/ChatCommand.php +++ b/app/TwitchBot/ChatCommand.php @@ -59,10 +59,15 @@ abstract class ChatCommand { } protected function listAnd($entries) { + $lang = empty($this->channels->languages) ? 'en' : $this->channel->languages[0]; + if ($lang == 'de') { + return Arr::join($entries, ', ', ' und '); + } return Arr::join($entries, ', ', ' and '); } protected function messageChannel($str) { + if (empty($str)) return; $msg = IRCMessage::privmsg($this->channel->twitch_chat, $str); $this->bot->sendIRCMessage($msg); } diff --git a/app/TwitchBot/GuessingCancelCommand.php b/app/TwitchBot/GuessingCancelCommand.php index c510a32..19f9953 100644 --- a/app/TwitchBot/GuessingCancelCommand.php +++ b/app/TwitchBot/GuessingCancelCommand.php @@ -7,8 +7,9 @@ class GuessingCancelCommand extends ChatCommand { public function execute($args) { if ($this->chanel->hasActiveGuessing()) { $this->channel->clearGuessing(); - $this->messageChannel('Guessing game cancelled'); } + $msg = $this->channel->getGuessingSetting('cancel_message'); + $this->messageChannel($msg); } } diff --git a/app/TwitchBot/GuessingSolveCommand.php b/app/TwitchBot/GuessingSolveCommand.php index 57ee5f8..b4a877d 100644 --- a/app/TwitchBot/GuessingSolveCommand.php +++ b/app/TwitchBot/GuessingSolveCommand.php @@ -6,15 +6,13 @@ class GuessingSolveCommand extends ChatCommand { public function execute($args) { if (!$this->channel->hasActiveGuessing()) { - $this->messageChannel('Channel has no active guessing game'); + $msg = $this->channel->getGuessingSetting('not_active_message'); + $this->messageChannel($msg); return; } - if (empty($args)) { - $this->messageChannel('Please provide a solution to the guessing game'); - return; - } - if (!$this->channel->isValidGuess($args)) { - $this->messageChannel('Please provide a valid solution to the guessing game'); + if (empty($args) || !$this->channel->isValidGuess($args)) { + $msg = $this->channel->getGuessingSetting('invalid_solution_message'); + $this->messageChannel($msg); return; } $winners = $this->channel->solveGuessing($args); @@ -25,9 +23,12 @@ class GuessingSolveCommand extends ChatCommand { } } if (empty($names)) { - $this->messageChannel('nobody wins :('); + $msg = $this->channel->getGuessingSetting('no_winners_message'); + $this->messageChannel($msg); } else { - $this->messageChannel('Congrats '.$this->listAnd($names)); + $msg = $this->channel->getGuessingSetting('winners_message'); + $msg = str_replace('{names}', $this->listAnd($names), $msg); + $this->messageChannel($msg); } $this->channel->clearGuessing(); } diff --git a/app/TwitchBot/GuessingStartCommand.php b/app/TwitchBot/GuessingStartCommand.php index d2b2168..64a5729 100644 --- a/app/TwitchBot/GuessingStartCommand.php +++ b/app/TwitchBot/GuessingStartCommand.php @@ -6,12 +6,14 @@ class GuessingStartCommand extends ChatCommand { public function execute($args) { if ($this->channel->hasActiveGuessing()) { - $this->messageChannel('Channel already has an active guessing game'); + $msg = $this->channel->getGuessingSetting('active_message'); + $this->messageChannel($msg); return; } $type = $this->getStringConfig('type', 'gtbk'); $this->channel->startGuessing($type); - $this->messageChannel('Get your guesses in'); + $msg = $this->channel->getGuessingSetting('start_message'); + $this->messageChannel($msg); } } diff --git a/app/TwitchBot/GuessingStopCommand.php b/app/TwitchBot/GuessingStopCommand.php index cc4cd08..898ece7 100644 --- a/app/TwitchBot/GuessingStopCommand.php +++ b/app/TwitchBot/GuessingStopCommand.php @@ -6,11 +6,13 @@ class GuessingStopCommand extends ChatCommand { public function execute($args) { if (!$this->channel->hasActiveGuessing()) { - $this->messageChannel('Channel has no active guessing game'); + $msg = $this->channel->getGuessingSetting('not_active_message'); + $this->messageChannel($msg); return; } $this->channel->stopGuessing(); - $this->messageChannel('Guessing closed'); + $msg = $this->channel->getGuessingSetting('stop_message'); + $this->messageChannel($msg); } } diff --git a/database/migrations/2024_02_28_155113_guessing_settings.php b/database/migrations/2024_02_28_155113_guessing_settings.php new file mode 100644 index 0000000..953ed1d --- /dev/null +++ b/database/migrations/2024_02_28_155113_guessing_settings.php @@ -0,0 +1,32 @@ +text('guessing_settings')->default('{}'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('channels', function (Blueprint $table) { + $table->dropColumn('guessing_settings'); + }); + } +}; diff --git a/resources/js/components/twitch-bot/Command.js b/resources/js/components/twitch-bot/Command.js new file mode 100644 index 0000000..9122086 --- /dev/null +++ b/resources/js/components/twitch-bot/Command.js @@ -0,0 +1,57 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { Button } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import Icon from '../common/Icon'; + +const Command = ({ + name, + onEditCommand, + onRemoveCommand, + settings, +}) => { + const { t } = useTranslation(); + + const type = (settings && settings.command) || 'none'; + + return + {`!${name}`} + {t(`twitchBot.commandRestrictions.${(settings && settings.restrict) || 'none'}`)} + {t(`twitchBot.commandTypes.${type}`)} + +
+ {onEditCommand ? + + : null} + {onRemoveCommand ? + + : null} +
+ + ; +}; + +Command.propTypes = { + name: PropTypes.string, + onEditCommand: PropTypes.func, + onRemoveCommand: PropTypes.func, + settings: PropTypes.shape({ + command: PropTypes.string, + restrict: PropTypes.string, + }), +}; + +export default Command; diff --git a/resources/js/components/twitch-bot/CommandDialog.js b/resources/js/components/twitch-bot/CommandDialog.js new file mode 100644 index 0000000..7e8ce16 --- /dev/null +++ b/resources/js/components/twitch-bot/CommandDialog.js @@ -0,0 +1,41 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { Modal } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import CommandForm from './CommandForm'; + +const CommandDialog = ({ + name, + onHide, + onSubmit, + settings, + show, +}) => { + const { t } = useTranslation(); + + return + + + {t(name ? 'twitchBot.commandDialog' : 'twitchBot.addCommand')} + + + + ; +}; + +CommandDialog.propTypes = { + name: PropTypes.string, + onHide: PropTypes.func, + onSubmit: PropTypes.func, + settings: PropTypes.shape({ + }), + show: PropTypes.bool, +}; + +export default CommandDialog; diff --git a/resources/js/components/twitch-bot/CommandForm.js b/resources/js/components/twitch-bot/CommandForm.js new file mode 100644 index 0000000..9e9586a --- /dev/null +++ b/resources/js/components/twitch-bot/CommandForm.js @@ -0,0 +1,163 @@ +import { withFormik } from 'formik'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { Button, Form, Modal } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import laravelErrorsToFormik from '../../helpers/laravelErrorsToFormik'; +import yup from '../../schema/yup'; + +const CommandForm = ({ + errors, + handleBlur, + handleChange, + handleSubmit, + name, + onCancel, + touched, + values, +}) => { + const { t } = useTranslation(); + + const COMMANDS = [ + 'none', + 'runner', + 'crew', + 'guessing-start', + 'guessing-stop', + 'guessing-solve', + 'guessing-cancel', + ]; + const RESTRICTIONS = [ + 'none', + 'mod', + 'owner', + ]; + + return
+ + + {t('twitchBot.commandName')} + + {touched.name && errors.name ? + + {t(errors.name)} + + : null} + + + {t('twitchBot.commandRestriction')} + + {RESTRICTIONS.map(r => + + )} + + {touched.restrict && errors.restrict ? + + {t(errors.restrict)} + + : null} + + + {t('twitchBot.commandType')} + + {COMMANDS.map(c => + + )} + + {touched.command && errors.command ? + + {t(errors.command)} + + : null} + + + + {onCancel ? + + : null} + + +
; +}; + +CommandForm.propTypes = { + errors: PropTypes.shape({ + command: PropTypes.string, + name: PropTypes.string, + restrict: PropTypes.string, + }), + handleBlur: PropTypes.func, + handleChange: PropTypes.func, + handleSubmit: PropTypes.func, + name: PropTypes.string, + onCancel: PropTypes.func, + touched: PropTypes.shape({ + command: PropTypes.bool, + name: PropTypes.bool, + restrict: PropTypes.bool, + }), + values: PropTypes.shape({ + command: PropTypes.string, + name: PropTypes.string, + restrict: PropTypes.string, + }), +}; + +export default withFormik({ + displayName: 'CommandForm', + enableReinitialize: true, + handleSubmit: async (values, actions) => { + const { setErrors } = actions; + const { onSubmit } = actions.props; + try { + await onSubmit(values); + } catch (e) { + if (e.response && e.response.data && e.response.data.errors) { + setErrors(laravelErrorsToFormik(e.response.data.errors)); + } + } + }, + mapPropsToValues: ({ name, settings }) => { + return { + command: (settings && settings.command) || 'none', + name: name || '', + restrict: (settings && settings.restrict) || 'none', + }; + }, + validationSchema: yup.object().shape({ + command: yup.string(), + name: yup.string().required(), + restrict: yup.string(), + }), +})(CommandForm); diff --git a/resources/js/components/twitch-bot/Commands.js b/resources/js/components/twitch-bot/Commands.js new file mode 100644 index 0000000..98600d0 --- /dev/null +++ b/resources/js/components/twitch-bot/Commands.js @@ -0,0 +1,49 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { Table } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import Command from './Command'; + +const Commands = ({ + channel, + onEditCommand, + onRemoveCommand, +}) => { + const { t } = useTranslation(); + + return channel.chat_commands ? + + + + + + + + + + + {Object.entries(channel.chat_commands).map(([name, settings]) => + + )} + +
{t('twitchBot.commandName')}{t('twitchBot.commandRestriction')}{t('twitchBot.commandType')}{t('general.actions')}
+ : null; +}; + +Commands.propTypes = { + channel: PropTypes.shape({ + chat_commands: PropTypes.shape({ + }), + }), + onEditCommand: PropTypes.func, + onRemoveCommand: PropTypes.func, +}; + +export default Commands; diff --git a/resources/js/components/twitch-bot/Controls.js b/resources/js/components/twitch-bot/Controls.js index bdc8938..822c201 100644 --- a/resources/js/components/twitch-bot/Controls.js +++ b/resources/js/components/twitch-bot/Controls.js @@ -5,12 +5,18 @@ import { useTranslation } from 'react-i18next'; import toastr from 'toastr'; import ChatSettingsForm from './ChatSettingsForm'; +import CommandDialog from './CommandDialog'; +import Commands from './Commands'; +import GuessingSettingsForm from './GuessingSettingsForm'; import ChannelSelect from '../common/ChannelSelect'; import ToggleSwitch from '../common/ToggleSwitch'; const Controls = () => { const [channel, setChannel] = React.useState(null); const [chatText, setChatText] = React.useState(''); + const [editCommand, setEditCommand] = React.useState(''); + const [editCommandSettings, setEditCommandSettings] = React.useState({}); + const [showCommandDialog, setShowCommandDialog] = React.useState(false); const { t } = useTranslation(); @@ -56,6 +62,59 @@ const Controls = () => { } }, [channel, t]); + const onAddCommand = React.useCallback(() => { + setEditCommand(''); + setEditCommandSettings({}); + setShowCommandDialog(true); + }, [channel]); + + const onEditCommand = React.useCallback((name, settings) => { + setEditCommand(name); + setEditCommandSettings(settings); + setShowCommandDialog(true); + }, [channel]); + + const onRemoveCommand = React.useCallback(async (name) => { + try { + const rsp = await axios.delete(`/api/channels/${channel.id}/commands/${name}`); + setChannel(rsp.data); + toastr.success(t('twitchBot.saveSuccess')); + } catch (e) { + toastr.error(t('twitchBot.saveError')); + } + }, [channel]); + + const saveCommand = React.useCallback(async (values) => { + try { + const rsp = await axios.put( + `/api/channels/${channel.id}/commands/${values.name}`, + values, + ); + setChannel(rsp.data); + setShowCommandDialog(false); + setEditCommand(''); + setEditCommandSettings({}); + toastr.success(t('twitchBot.saveSuccess')); + } catch (e) { + toastr.error(t('twitchBot.saveError')); + throw e; + } + }, [channel]); + + const saveGuessingGame = React.useCallback(async (values) => { + try { + const rsp = await axios.put( + `/api/channels/${channel.id}/guessing-game/${values.name}`, + values, + ); + setChannel(rsp.data); + toastr.success(t('twitchBot.saveSuccess')); + } catch (e) { + toastr.error(t('twitchBot.saveError')); + throw e; + } + }, [channel]); + return <> @@ -105,41 +164,76 @@ const Controls = () => { {channel ? - - {t('twitchBot.chat')} - { - setChatText(value); + +

{t('twitchBot.chat')}

+ + {t('twitchBot.chat')} + { + setChatText(value); + }} + value={chatText} + /> +
+ + +
+
+ + +

{t('twitchBot.chatSettings')}

+ + + +

{t('twitchBot.commands')}

+ + { + setShowCommandDialog(false); + setEditCommand(''); + setEditCommandSettings({}); }} - value={chatText} + onSubmit={saveCommand} + settings={editCommandSettings} + show={showCommandDialog} /> -
- -
-
- -

{t('twitchBot.chatSettings')}

- + + +

{t('twitchBot.guessingGame.settings')}

+
: diff --git a/resources/js/components/twitch-bot/GuessingSettingsForm.js b/resources/js/components/twitch-bot/GuessingSettingsForm.js new file mode 100644 index 0000000..128d6f5 --- /dev/null +++ b/resources/js/components/twitch-bot/GuessingSettingsForm.js @@ -0,0 +1,373 @@ +import { withFormik } from 'formik'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { Button, Col, Form, Row } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import laravelErrorsToFormik from '../../helpers/laravelErrorsToFormik'; +import i18n from '../../i18n'; +import yup from '../../schema/yup'; + +const GuessingSettingsForm = ({ + errors, + handleBlur, + handleChange, + handleSubmit, + onCancel, + touched, + values, +}) => { + const { t } = useTranslation(); + + return
+ + + {t('twitchBot.guessingGame.pointsExactFirst')} + + {touched.points_exact_first && errors.points_exact_first ? + + {t(errors.points_exact_first)} + + : null} + + + {t('twitchBot.guessingGame.pointsExactOther')} + + {touched.points_exact_other && errors.points_exact_other ? + + {t(errors.points_exact_other)} + + : null} + + + {t('twitchBot.guessingGame.pointsCloseFirst')} + + {touched.points_close_first && errors.points_close_first ? + + {t(errors.points_close_first)} + + : null} + + + {t('twitchBot.guessingGame.pointsCloseOther')} + + {touched.points_close_other && errors.points_close_other ? + + {t(errors.points_close_other)} + + : null} + + + {t('twitchBot.guessingGame.pointsCloseMax')} + + {touched.points_close_max && errors.points_close_max ? + + {t(errors.points_close_max)} + + : null} + + + + {t('twitchBot.guessingGame.startMessage')} + + {touched.start_message && errors.start_message ? + + {t(errors.start_message)} + + : null} + + + {t('twitchBot.guessingGame.stopMessage')} + + {touched.stop_message && errors.stop_message ? + + {t(errors.stop_message)} + + : null} + + + {t('twitchBot.guessingGame.winnersMessage')} + + {touched.winners_message && errors.winners_message ? + + {t(errors.winners_message)} + + : + + {t('twitchBot.guessingGame.winnersMessageHint')} + + } + + + {t('twitchBot.guessingGame.noWinnersMessage')} + + {touched.no_winners_message && errors.no_winners_message ? + + {t(errors.no_winners_message)} + + : null} + + + {t('twitchBot.guessingGame.cancelMessage')} + + {touched.cancel_message && errors.cancel_message ? + + {t(errors.cancel_message)} + + : null} + + + {t('twitchBot.guessingGame.invalidSolutionMessage')} + + {touched.invalid_solution_message && errors.invalid_solution_message ? + + {t(errors.invalid_solution_message)} + + : null} + + + {t('twitchBot.guessingGame.activeMessage')} + + {touched.active_message && errors.active_message ? + + {t(errors.active_message)} + + : null} + + + {t('twitchBot.guessingGame.notActiveMessage')} + + {touched.not_active_message && errors.not_active_message ? + + {t(errors.not_active_message)} + + : null} + +
+ {onCancel ? + + : null} + +
+
; +}; + +GuessingSettingsForm.propTypes = { + errors: PropTypes.shape({ + active_message: PropTypes.string, + cancel_message: PropTypes.string, + invalid_solution_message: PropTypes.string, + name: PropTypes.string, + no_winners_message: PropTypes.string, + not_active_message: PropTypes.string, + points_close_first: PropTypes.string, + points_close_max: PropTypes.string, + points_close_other: PropTypes.string, + points_exact_first: PropTypes.string, + points_exact_other: PropTypes.string, + start_message: PropTypes.string, + stop_message: PropTypes.string, + winners_message: PropTypes.string, + }), + handleBlur: PropTypes.func, + handleChange: PropTypes.func, + handleSubmit: PropTypes.func, + name: PropTypes.string, + onCancel: PropTypes.func, + touched: PropTypes.shape({ + active_message: PropTypes.bool, + cancel_message: PropTypes.bool, + invalid_solution_message: PropTypes.bool, + name: PropTypes.bool, + no_winners_message: PropTypes.bool, + not_active_message: PropTypes.bool, + points_close_first: PropTypes.bool, + points_close_max: PropTypes.bool, + points_close_other: PropTypes.bool, + points_exact_first: PropTypes.bool, + points_exact_other: PropTypes.bool, + start_message: PropTypes.bool, + stop_message: PropTypes.bool, + winners_message: PropTypes.bool, + }), + values: PropTypes.shape({ + active_message: PropTypes.string, + cancel_message: PropTypes.string, + invalid_solution_message: PropTypes.string, + name: PropTypes.string, + no_winners_message: PropTypes.string, + not_active_message: PropTypes.string, + points_close_first: PropTypes.number, + points_close_max: PropTypes.number, + points_close_other: PropTypes.number, + points_exact_first: PropTypes.number, + points_exact_other: PropTypes.number, + start_message: PropTypes.string, + stop_message: PropTypes.string, + winners_message: PropTypes.string, + }), +}; + +export default withFormik({ + displayName: 'GuessingSettingsForm', + enableReinitialize: true, + handleSubmit: async (values, actions) => { + const { setErrors } = actions; + const { onSubmit } = actions.props; + try { + await onSubmit(values); + } catch (e) { + if (e.response && e.response.data && e.response.data.errors) { + setErrors(laravelErrorsToFormik(e.response.data.errors)); + } + } + }, + mapPropsToValues: ({ name, settings }) => { + const getNumericValue = (s, n, d) => s && Object.prototype.hasOwnProperty.call(s, n) + ? s[n] : d; + const getStringValue = (s, n, d) => s && Object.prototype.hasOwnProperty.call(s, n) + ? s[n] : i18n.t(`twitchBot.guessingGame.default${d}`); + return { + active_message: getStringValue(settings, 'active_message', 'ActiveMessage'), + cancel_message: getStringValue(settings, 'cancel_message', 'CancelMessage'), + invalid_solution_message: + getStringValue(settings, 'invalid_solution_message', 'InvalidSolutionMessage'), + name: name || '', + no_winners_message: getStringValue(settings, 'no_winners_message', 'NoWinnersMessage'), + not_active_message: getStringValue(settings, 'not_active_message', 'NotActiveMessage'), + points_close_first: getNumericValue(settings, 'points_close_first', 1), + points_close_max: getNumericValue(settings, 'points_close_max', 3), + points_close_other: getNumericValue(settings, 'points_close_other', 1), + points_exact_first: getNumericValue(settings, 'points_exact_first', 1), + points_exact_other: getNumericValue(settings, 'points_exact_other', 1), + start_message: getStringValue(settings, 'start_message', 'StartMessage'), + stop_message: getStringValue(settings, 'stop_message', 'StopMessage'), + winners_message: getStringValue(settings, 'winners_message', 'WinnersMessage'), + }; + }, + validationSchema: yup.object().shape({ + active_message: yup.string(), + cancel_message: yup.string(), + invalid_solution_message: yup.string(), + name: yup.string().required(), + no_winners_message: yup.string(), + not_active_message: yup.string(), + points_close_first: yup.number(), + points_close_max: yup.number(), + points_close_other: yup.number(), + points_exact_first: yup.number(), + points_exact_other: yup.number(), + start_message: yup.string(), + stop_message: yup.string(), + winners_message: yup.string(), + }), +})(GuessingSettingsForm); diff --git a/resources/js/i18n/de.js b/resources/js/i18n/de.js index 080a6dd..b949695 100644 --- a/resources/js/i18n/de.js +++ b/resources/js/i18n/de.js @@ -182,6 +182,7 @@ export default { tech: 'ALttP Techniken', }, general: { + actions: 'Aktionen', anonymous: 'Anonym', appDescription: 'Turniere und Tutorials für The Legend of Zelda: A Link to the Past Randomizer', appName: 'ALttP', @@ -491,6 +492,7 @@ export default { unlockSuccess: 'Turnier entsperrt', }, twitchBot: { + addCommand: 'Command hinzufügen', channel: 'Channel', chat: 'Chat', chatError: 'Fehler beim Senden', @@ -500,7 +502,52 @@ export default { chatWaitMsgsMax: 'Max. Messages', chatWaitTimeMin: 'Min. Zeit', chatWaitTimeMax: 'Max. Zeit', + commandDialog: 'Command bearbeiten', + commandName: 'Name', + commandParameters: 'Parameter', + commandRestriction: 'Einschränkung', + commandRestrictions: { + mod: 'Mods', + none: 'keine', + owner: 'Owner', + }, + commands: 'Commands', + commandType: 'Typ', + commandTypes: { + crew: 'Crew Liste', + 'guessing-cancel': 'Guessing Game abbrechen', + 'guessing-solve': 'Guessing Game auflösen', + 'guessing-start': 'Guessing Game starten', + 'guessing-stop': 'Guessing Game stoppen', + none: 'keiner', + runner: 'Runner Liste', + }, controls: 'Controls', + guessingGame: { + activeMessage: 'Nachricht bei bereits laufendem Spiel', + cancelMessage: 'Nachricht bei Spielabbruch', + defaultActiveMessage: 'Es läuft bereits ein Spiel auf diesem Kanal', + defaultCancelMessage: 'Spiel abgebrochen', + defaultInvalidSolutionMessage: 'Bitte eine gültige Lösung für das Guessing Game angeben', + defaultNoWinnersMessage: 'keiner gewonnen :(', + defaultNotActiveMessage: 'Es läuft gerade kein Spiel auf diesem Kanal', + defaultStartMessage: 'Haut jetzt eure Zahlen raus!', + defaultStopMessage: 'Annahme geschlossen', + defaultWinnersMessage: 'Glückwunsch {names}!', + invalidSolutionMessage: 'Nachricht bei ungültiger (oder fehlender) Lösung', + noWinnersMessage: 'Nachricht, falls keine Gewinner', + notActiveMessage: 'Nachricht, wenn kein Spiel läuft', + pointsCloseFirst: 'Punkte für den ersten nächsten Treffer', + pointsCloseMax: 'Maximaler Abstand, um als nächster Treffer gewertet zu werden', + pointsCloseOther: 'Punkte für weitere nächste Treffer', + pointsExactFirst: 'Punkte für den ersten exakten Treffer', + pointsExactOther: 'Punkte für weitere exakte Treffer', + settings: 'Guessing Game Einstellungen', + startMessage: 'Nachricht bei Start', + stopMessage: 'Nachricht bei Einsendeschluss', + winnersMessage: 'Ankündigung der Gewinner', + winnersMessageHint: '{names} wird durch die Namen der Gewinner ersetzt', + }, heading: 'Twitch Bot', joinApp: 'Join als App Bot', joinChat: 'Join als Chat Bot', diff --git a/resources/js/i18n/en.js b/resources/js/i18n/en.js index f722332..695530d 100644 --- a/resources/js/i18n/en.js +++ b/resources/js/i18n/en.js @@ -182,6 +182,7 @@ export default { tech: 'ALttP Tech', }, general: { + actions: 'Actions', anonymous: 'Anonym', appDescription: 'Tournaments and tutorials for The Legend of Zelda: A Link to the Past Randomizer', appName: 'ALttP', @@ -491,6 +492,7 @@ export default { unlockSuccess: 'Tournament unlocked', }, twitchBot: { + addCommand: 'Add command', channel: 'Channel', chat: 'Chat', chatError: 'Error sending message', @@ -500,7 +502,52 @@ export default { chatWaitMsgsMax: 'Max. messages', chatWaitTimeMin: 'Min. time', chatWaitTimeMax: 'Max. time', + commandDialog: 'Edit command', + commandName: 'Name', + commandParameters: 'Parameters', + commandRestriction: 'Restriction', + commandRestrictions: { + mod: 'Mods', + none: 'none', + owner: 'Owner', + }, + commands: 'Commands', + commandType: 'Type', + commandTypes: { + crew: 'Crew list', + 'guessing-cancel': 'Cancel guessing game', + 'guessing-solve': 'Solve guessing game', + 'guessing-start': 'Start guessing game', + 'guessing-stop': 'Stop guessing game', + none: 'keiner', + runner: 'Runner list', + }, controls: 'Controls', + guessingGame: { + activeMessage: 'Message when a game is already running', + cancelMessage: 'Game cancellation announcement', + defaultActiveMessage: 'Channel already has an active guessing game', + defaultCancelMessage: 'Guessing game cancelled', + defaultInvalidSolutionMessage: 'Please provide a valid solution to the guessing game', + defaultNoWinnersMessage: 'nobody wins :(', + defaultNotActiveMessage: 'Channel has no active guessing game', + defaultStartMessage: 'Get your guesses in', + defaultStopMessage: 'Guessing closed', + defaultWinnersMessage: 'Congrats {names}!', + invalidSolutionMessage: 'Message for invalid (or missing) solution', + noWinnersMessage: 'Announcement for no winners', + notActiveMessage: 'Message when no game is currently active', + pointsCloseFirst: 'Points for first close match', + pointsCloseMax: 'Maximum distance to count as close match', + pointsCloseOther: 'Points for further close matches', + pointsExactFirst: 'Points for first exact match', + pointsExactOther: 'Points for further exact matches', + settings: 'Guessing game settings', + startMessage: 'Starting announcement', + stopMessage: 'Closing announcement', + winnersMessage: 'Winners announcement', + winnersMessageHint: '{names} will be replaced with a list of winners\' names', + }, heading: 'Twitch Bot', joinApp: 'Join as App Bot', joinChat: 'Join as Chat Bot', diff --git a/routes/api.php b/routes/api.php index 84cbf73..276ff63 100644 --- a/routes/api.php +++ b/routes/api.php @@ -30,6 +30,9 @@ Route::post('channels/{channel}/chat', 'App\Http\Controllers\ChannelController@c Route::post('channels/{channel}/chat-settings', 'App\Http\Controllers\ChannelController@chatSettings'); Route::post('channels/{channel}/join', 'App\Http\Controllers\ChannelController@join'); Route::post('channels/{channel}/part', 'App\Http\Controllers\ChannelController@part'); +Route::delete('channels/{channel}/commands/{command}', 'App\Http\Controllers\ChannelController@deleteCommand'); +Route::put('channels/{channel}/commands/{command}', 'App\Http\Controllers\ChannelController@saveCommand'); +Route::put('channels/{channel}/guessing-game/{name}', 'App\Http\Controllers\ChannelController@saveGuessingGame'); Route::get('content', 'App\Http\Controllers\TechniqueController@search'); Route::get('content/{tech:name}', 'App\Http\Controllers\TechniqueController@single');