diff --git a/VERSION b/VERSION index 6f165bc1..69669de6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.12.1 \ No newline at end of file +1.12.2 \ No newline at end of file diff --git a/acp/package.json b/acp/package.json index c375cc2f..cb7abeda 100644 --- a/acp/package.json +++ b/acp/package.json @@ -1,6 +1,6 @@ { "name": "mnml-client", - "version": "1.12.1", + "version": "1.12.2", "description": "", "main": "index.js", "scripts": { diff --git a/bin/version.sh b/bin/version.sh index 673f5e76..d659bd40 100755 --- a/bin/version.sh +++ b/bin/version.sh @@ -15,3 +15,5 @@ cd $MNML_PATH/ops && npm --allow-same-version --no-git-tag-version version "$VER cd $MNML_PATH/client && npm --allow-same-version --no-git-tag-version version "$VERSION" cd $MNML_PATH/acp && npm --allow-same-version --no-git-tag-version version "$VERSION" cd $MNML_PATH/studios && npm --allow-same-version --no-git-tag-version version "$VERSION" + +git commit -am "v$VERSION" \ No newline at end of file diff --git a/client/assets/styles/menu.less b/client/assets/styles/menu.less index ff035799..c9890991 100644 --- a/client/assets/styles/menu.less +++ b/client/assets/styles/menu.less @@ -112,7 +112,7 @@ section { figure { letter-spacing: 0.25em; text-transform: uppercase; - font-size: 125%; + font-size: 1.5em; display: flex; flex-flow: column; } @@ -138,27 +138,14 @@ section { grid-template-columns: 1fr; } - button.ready:enabled { - color: forestgreen; - border-color: forestgreen; - - &:hover { - background: forestgreen; - color: black; - border-color: forestgreen; - } - } - - // // all green // button.ready:enabled { - // background: forestgreen; - // color: black; + // color: forestgreen; // border-color: forestgreen; // &:hover { - // color: forestgreen; + // background: forestgreen; + // color: black; // border-color: forestgreen; - // background: 0; // } // } } diff --git a/client/assets/styles/styles.less b/client/assets/styles/styles.less index fb8b1072..f2576ff4 100644 --- a/client/assets/styles/styles.less +++ b/client/assets/styles/styles.less @@ -173,6 +173,19 @@ button, input { // &:active { // filter: url("#noiseFilter"); // } + + // all green + &.ready:enabled { + background: forestgreen; + color: black; + border-color: forestgreen; + + &:hover { + color: forestgreen; + border-color: forestgreen; + background: 0; + } + } } a { @@ -269,6 +282,10 @@ figure.gray { @media (max-width: 1500px) { #mnml { font-size: 75%; + + &.front-page main { + padding: 0 10%; + } } svg { diff --git a/client/package.json b/client/package.json index b6ace460..977344f7 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "mnml-client", - "version": "1.12.1", + "version": "1.12.2", "description": "", "main": "index.js", "scripts": { diff --git a/client/src/components/anims/amplify.jsx b/client/src/components/anims/amplify.jsx index 9dc087a0..6e77cc2c 100644 --- a/client/src/components/anims/amplify.jsx +++ b/client/src/components/anims/amplify.jsx @@ -22,7 +22,7 @@ class Amplify extends Component { xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200"> - + diff --git a/client/src/components/anims/source.cast.jsx b/client/src/components/anims/source.cast.jsx index 25237776..83f30805 100644 --- a/client/src/components/anims/source.cast.jsx +++ b/client/src/components/anims/source.cast.jsx @@ -6,8 +6,8 @@ function sourceCast(id, direction, idle) { const { x, y } = direction; return anime({ targets: [document.getElementById(id)], - translateX: x * window.screen.width * 0.1, - translateY: y * window.screen.height * 0.1, + translateX: x * window.innerWidth * 0.1, + translateY: y * window.innerHeight * 0.1, easing: 'easeInOutElastic', direction: 'alternate', duration: TIMES.SOURCE_DURATION_MS, diff --git a/client/src/components/construct.jsx b/client/src/components/construct.jsx index ec3d7813..0c979107 100644 --- a/client/src/components/construct.jsx +++ b/client/src/components/construct.jsx @@ -69,7 +69,11 @@ class ConstructAvatar extends Component { const type = resolution.event[0]; // only trigger the wiggle on damage and ko events rather than spam it on everything // also stops wiggle triggering when invert effect is applied - if (['Damage', 'Ko'].includes(type)) return wiggle(construct.id, this.idle); + const wiggleEvents = [ + 'Damage', + // 'Ko' + ]; + if (wiggleEvents.includes(type)) return wiggle(construct.id, this.idle); } // different source object and source construct diff --git a/client/src/components/game.construct.anim.text.jsx b/client/src/components/game.construct.anim.text.jsx index 8b982cd2..670827d3 100644 --- a/client/src/components/game.construct.anim.text.jsx +++ b/client/src/components/game.construct.anim.text.jsx @@ -63,6 +63,8 @@ class AnimText extends preact.Component { } case 'Ko': return

KO!

; case 'Reflection': return

REFLECT

; + case 'CooldownIncrease': return

+{event.turns}T cooldowns

; + case 'CooldownDecrease': return

-{event.turns}T cooldowns

; default: return false; } }; diff --git a/client/src/components/game.construct.skill.btn.jsx b/client/src/components/game.construct.skill.btn.jsx index cedf7c33..b0cf1995 100644 --- a/client/src/components/game.construct.skill.btn.jsx +++ b/client/src/components/game.construct.skill.btn.jsx @@ -82,9 +82,11 @@ function Skill(props) { const border = buttons[removeTier(s.skill)] ? buttons[removeTier(s.skill)]() : ''; + const notSkill = game.phase !== 'Skill'; + return ( - + ); } diff --git a/client/src/components/play.jsx b/client/src/components/play.jsx index e731ad5c..1450a2df 100644 --- a/client/src/components/play.jsx +++ b/client/src/components/play.jsx @@ -95,7 +95,7 @@ function Play(args) { type="submit"> Invite -
Invite a Friend
+
Play against friend
); @@ -221,13 +221,15 @@ function Play(args) {

¤ {account.balance}

- {subscription} - +
{subscription}
+
+ +
Join our Discord server to find opponents and talk to the devs.
diff --git a/client/src/components/reshape.jsx b/client/src/components/reshape.jsx index 835f4a8e..2bfb4b11 100644 --- a/client/src/components/reshape.jsx +++ b/client/src/components/reshape.jsx @@ -89,6 +89,7 @@ function Reshape(args) { return (
setMtxActive(null)}>
+

Customise your Constructs

Use credits to modify your construct names and appearance.

    @@ -106,19 +107,19 @@ function Reshape(args) {

    ¤ {account.balance}

    - {subscription} - -
    -
    -
    +
    {subscription}
    +
    + +
    {shop.owned.map(useMtx)} {shop.available.map(availableMtx)}
    +
); diff --git a/client/src/components/shop.jsx b/client/src/components/shop.jsx index 7e23bc60..ca7121c8 100644 --- a/client/src/components/shop.jsx +++ b/client/src/components/shop.jsx @@ -27,7 +27,7 @@ function Shop(args) { return (
-

Support the game

+

Support MNML

Credits are in game currency used to change your team appearance:

    diff --git a/client/src/components/vbox.info.jsx b/client/src/components/vbox.info.jsx index d2eddbec..126780be 100644 --- a/client/src/components/vbox.info.jsx +++ b/client/src/components/vbox.info.jsx @@ -5,8 +5,8 @@ const { tutorialStage } = require('../tutorial.utils'); const { genItemInfo } = require('./vbox.utils'); const addState = connect( - ({ info, player, tutorial, vboxInfo, itemInfo, instance, comboPreview }) => ({ - info, player, tutorial, vboxInfo, itemInfo, instance, comboPreview, + ({ info, player, tutorial, vboxInfo, itemInfo, instance, comboPreview, authenticated }) => ({ + info, player, tutorial, vboxInfo, itemInfo, instance, comboPreview, authenticated })); @@ -35,12 +35,13 @@ class Info extends preact.Component { itemInfo, instance, comboPreview, + authenticated, } = props; // dispaly priority // tutorial -> comboPreview -> vboxInfo -> info if (tutorial) { - const tutorialStageInfo = tutorialStage(tutorial, clearTutorial, instance); + const tutorialStageInfo = tutorialStage(authenticated, tutorial, clearTutorial, instance); if (tutorialStageInfo) return tutorialStageInfo; } if (comboPreview) return genItemInfo(comboPreview, itemInfo, player); diff --git a/client/src/components/vbox.stash.jsx b/client/src/components/vbox.stash.jsx index cec94686..0e737627 100644 --- a/client/src/components/vbox.stash.jsx +++ b/client/src/components/vbox.stash.jsx @@ -9,7 +9,7 @@ const buttons = require('./buttons'); const { removeTier } = require('../utils'); const addState = connect( - ({ itemUnequip, vboxHighlight, vboxSelected }) => ({ itemUnequip, vboxHighlight, vboxSelected })); + ({ itemUnequip, vboxHighlight, vboxSelected, tutorial }) => ({ itemUnequip, vboxHighlight, vboxSelected, tutorial })); class stashElement extends preact.Component { shouldComponentUpdate(newProps) { @@ -23,6 +23,7 @@ class stashElement extends preact.Component { if (newProps.itemUnequip !== this.props.itemUnequip) return true; if (newProps.vboxHighlight !== this.props.vboxHighlight) return true; if (newProps.vboxSelected !== this.props.vboxSelected) return true; + if (newProps.tutorial !== this.props.tutorial) return true; return false; } @@ -39,6 +40,7 @@ class stashElement extends preact.Component { itemUnequip, vboxHighlight, vboxSelected, + tutorial, } = props; const { storeSelect, stashSelect } = vboxSelected; @@ -95,7 +97,7 @@ class stashElement extends preact.Component { : `${border} ${notValidCombo ? 'fade' : ''}`; const invObject = shapes[v] ? shapes[v]() : v; - + const tutorialDisable = tutorial === 1; return ( diff --git a/client/src/events.jsx b/client/src/events.jsx index 48028205..ce566608 100644 --- a/client/src/events.jsx +++ b/client/src/events.jsx @@ -77,21 +77,24 @@ function registerEvents(store) { if (game && currentGame) { if (game.resolutions.length !== currentGame.resolutions.length) { + // stop fetching the game state til animations are done store.dispatch(actions.setAnimating(true)); store.dispatch(actions.setGameSkillInfo(null)); - // stop fetching the game state til animations are done - const newRes = game.resolutions[game.resolutions.length - 1]; - return eachSeries(newRes, (r, cb) => { - if (r.delay === 0) return cb(); // TargetKo etc - setAnimations(r, store); - return setTimeout(cb, r.delay); + + const newTurns = game.resolutions.slice(currentGame.resolutions.length); + return eachSeries(newTurns, (turn, turnCb) => { + return eachSeries(turn, (r, cb) => { + if (r.delay === 0) return cb(); // TargetKo etc + setAnimations(r, store); + return setTimeout(cb, r.delay); + }, turnCb); }, err => { if (err) return console.error(err); clearAnimations(store); // set the game state so resolutions don't fire twice store.dispatch(actions.setGame(game)); ws.sendGameState(game.id); - return true; + return false; }); } } @@ -113,7 +116,6 @@ function registerEvents(store) { } store.dispatch(actions.setAccount(account)); - store.dispatch(actions.setTutorial(null)); store.dispatch(actions.setAuthenticated(true)); } @@ -178,7 +180,7 @@ function registerEvents(store) { setPvp(false); const player = v.players.find(p => p.id === account.id); store.dispatch(actions.setPlayer(player)); - + if (tutorial) tutorialVbox(player, store, tutorial); if (v.phase === 'Finished') { @@ -186,7 +188,6 @@ function registerEvents(store) { } } - return store.dispatch(actions.setInstance(v)); } diff --git a/client/src/tutorial.utils.jsx b/client/src/tutorial.utils.jsx index 90c0ded5..410b7cf7 100644 --- a/client/src/tutorial.utils.jsx +++ b/client/src/tutorial.utils.jsx @@ -106,7 +106,7 @@ function tutorialVbox(player, store, tutorial) { store.dispatch(actions.setTutorial(stage)); } -function tutorialStage(tutorial, clearTutorial, instance) { +function tutorialStage(authenticated, tutorial, clearTutorial, instance) { if (!(instance.time_control === 'Practice' && instance.rounds.length === 1)) return false; const exit = () => clearTutorial(); @@ -116,10 +116,9 @@ function tutorialStage(tutorial, clearTutorial, instance) { return (

    Welcome to MNML

    -

    This is the VBOX Phase tutorial.

    -

    In the VBOX Phase you customise your constructs' skills and specialisations.

    -

    Colours are used to create powerful combinations with base items.

    +

    This is the VBOX Phase where you customise your team.

    Buy the two colours from the store to continue.

    +

    PRO TIP: While selecting an item in the shop, click the stash to buy it.

    ); } @@ -128,8 +127,9 @@ function tutorialStage(tutorial, clearTutorial, instance) { return (

    Combining Items

    -

    You start the game with the base Attack skill item.

    -

    Highlight the Attack and the two colours then click combine

    +

    You start the game with the Attack base skill item.

    +

    Create powerful combinations by combining colours with base items.

    +

    Select all three items to combine.

    ); } @@ -141,8 +141,7 @@ function tutorialStage(tutorial, clearTutorial, instance) {

    Equipping Items

    The first construct on your team is {constructOne}.

    Skill items can be equipped to your constructs to be used in the combat phase.

    -

    Click your new skill from the stash.
    - Once selected click the flashing SKILL slot or the construct img to equip the skill.

    +

    Select your new skill from the stash and then click the flashing SKILL slot or the construct image to equip.

); } @@ -162,11 +161,10 @@ function tutorialStage(tutorial, clearTutorial, instance) { return (

Specialisations

-

Equipping specialisation items will increase the stats of your constructs.

-

These can also be combined with colours for further specialisation.

-

Click the specialisation item in the stash.
- Once selected click the flashing SPEC slot to equip the specialisation.

-

PRO TIP: while selecting an item in the shop, click on your construct to buy and equip in one step.

+

Specialisation items increase the stats of your constructs.
+ They can be combined further with colours to optimise your team strategy.

+

Slect the item in the stash, once selected click the flashing SPEC slot to equip.

+

PRO TIP: While selecting an item in the shop, click on your construct to buy and equip in one step.

); } @@ -178,8 +176,7 @@ function tutorialStage(tutorial, clearTutorial, instance) {

Skills

You have now created a construct with an upgraded skill and base spec.

-

The goal is to create three powerful constructs for combat.

-

Equip your other constructs {constructTwo} and {constructThree} with the Attack skill.
+

Equip your other constructs {constructTwo} and {constructThree} with skills.
Ensure each construct has a single skill to continue.

PRO TIP: Select a skill or spec on a construct and click another construct to swap it.

@@ -203,7 +200,6 @@ function tutorialStage(tutorial, clearTutorial, instance) { if (window.innerWidth < 1000) { return exit(); } - return (

GLHF

@@ -213,6 +209,7 @@ function tutorialStage(tutorial, clearTutorial, instance) {
); } + return false; }; @@ -223,11 +220,18 @@ function tutorialStage(tutorial, clearTutorial, instance) { onMouseDown={exit}> Continue : null; + const skipTutorial = authenticated && !exitTutorial ? + + : null; + return (
{tutorialText()}
{exitTutorial} + {skipTutorial}
); } diff --git a/core/Cargo.toml b/core/Cargo.toml index febac315..4f9ed720 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mnml_core" -version = "1.12.1" +version = "1.12.2" authors = ["ntr ", "mashy "] [dependencies] diff --git a/core/src/construct.rs b/core/src/construct.rs index 2c45b7be..16572a8b 100644 --- a/core/src/construct.rs +++ b/core/src/construct.rs @@ -470,24 +470,26 @@ impl Construct { } pub fn increase_cooldowns(&mut self, turns: usize) -> Vec { - let mut events = vec![]; - + let mut cd_event = false; for skill in self.skills.iter_mut() { if skill.skill.base_cd().is_some() { // if has a cooldown + cd_event = true; match skill.cd { Some(cd) => { skill.cd = Some(cd.saturating_add(turns)); - events.push(Event::CooldownIncrease { construct: self.id, turns }) }, None => { skill.cd = Some(turns); - events.push(Event::CooldownIncrease { construct: self.id, turns }) }, } } } - return events; + if cd_event { + return vec![Event::CooldownIncrease { construct: self.id, turns }]; + } + + return vec![]; } pub fn reduce_cooldowns(&mut self) -> &mut Construct { @@ -909,7 +911,7 @@ impl Construct { construct: self.id, amount: healing, overhealing, - colour: Colour::Red, + colour: Colour::Blue, display: EventConstruct::new(self), }); } @@ -931,7 +933,7 @@ impl Construct { construct: self.id, amount: blue_damage_amount, mitigation: blue_mitigation, - colour: Colour::Red, + colour: Colour::Blue, display: EventConstruct::new(self), }); } diff --git a/core/src/skill.rs b/core/src/skill.rs index f26729a5..765b6930 100644 --- a/core/src/skill.rs +++ b/core/src/skill.rs @@ -2082,7 +2082,7 @@ impl Purify { fn purify(cast: Cast, game: &mut Game, values: Purify) { let gp = game.value(Value::Stat { construct: cast.source, stat: Stat::GreenPower }); - let rms = game.value(Value::Removals { construct: cast.target }); + let rms = game.value(Value::Effects { construct: cast.target }); let amount = gp.pct(values.green_heal_base().saturating_mul(rms)); game.action(cast, diff --git a/ops/package.json b/ops/package.json index 01e81dc9..d1d30272 100644 --- a/ops/package.json +++ b/ops/package.json @@ -1,6 +1,6 @@ { "name": "mnml-ops", - "version": "1.12.1", + "version": "1.12.2", "description": "", "main": "index.js", "scripts": { diff --git a/server/Cargo.toml b/server/Cargo.toml index a89622ea..76a849df 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mnml" -version = "1.12.1" +version = "1.12.2" authors = ["ntr "] [dependencies] diff --git a/server/src/account.rs b/server/src/account.rs index f180cf70..14812bcc 100644 --- a/server/src/account.rs +++ b/server/src/account.rs @@ -530,7 +530,7 @@ pub fn img_check(account: &Account) -> Result { } } -pub fn tutorial(tx: &mut Transaction, account: &Account) -> Result, Error> { +pub fn _tutorial(tx: &mut Transaction, account: &Account) -> Result, Error> { let query = " SELECT count(id) FROM players diff --git a/server/src/http.rs b/server/src/http.rs index 34b3b994..bb0d443d 100644 --- a/server/src/http.rs +++ b/server/src/http.rs @@ -25,7 +25,7 @@ use payments::{stripe}; pub const TOKEN_HEADER: &str = "x-auth-token"; pub const AUTH_CLEAR: &str = - "x-auth-token=; HttpOnly; SameSite=Strict; Path=/; Max-Age=-1;"; + "x-auth-token=; HttpOnly; SameSite=None; Path=/; Max-Age=-1;"; #[derive(Clone, Copy, Fail, Debug, Serialize, Deserialize)] pub enum MnmlHttpError { @@ -191,7 +191,7 @@ impl AfterMiddleware for ErrorHandler { fn token_res(token: String) -> Response { let v = Cookie::build(TOKEN_HEADER, token) .http_only(true) - .same_site(SameSite::Strict) + .same_site(SameSite::None) .path("/") .max_age(Duration::weeks(1)) // 1 week aligns with db set .finish(); @@ -354,7 +354,7 @@ fn recover(req: &mut Request) -> IronResult { let v = Cookie::build(TOKEN_HEADER, token) .http_only(true) - .same_site(SameSite::Strict) + .same_site(SameSite::None) .path("/") .max_age(Duration::weeks(1)) // 1 week aligns with db set .finish(); diff --git a/server/src/mail.rs b/server/src/mail.rs index 8f3b351f..0322d31c 100644 --- a/server/src/mail.rs +++ b/server/src/mail.rs @@ -232,6 +232,12 @@ pub fn set(tx: &mut Transaction, account: Uuid, email: &String) -> Result<(Uuid, RETURNING id; "; + let select_query = " + SELECT * + FROM emails + WHERE account = $1; + "; + let update_query = " UPDATE emails SET email = $1, confirm_token = $2, confirmed = false, recover_token = $3 @@ -239,18 +245,11 @@ pub fn set(tx: &mut Transaction, account: Uuid, email: &String) -> Result<(Uuid, RETURNING id; "; - let result = match tx.query(insert_query, &[&id, &account, &email, &confirm_token, &recover_token]) { - Ok(r) => r, - // email update probably - Err(_) => { - match tx.query(update_query, &[&email, &confirm_token, &recover_token, &account]) { - Ok(r) => r, - Err(e) => { - warn!("{:?}", e); - return Err(err_msg("no email set")); - }, - } - } + let existing = tx.query(select_query, &[&id])?; + + let result = match existing.iter().next() { + Some(_) => tx.query(insert_query, &[&id, &account, &email, &confirm_token, &recover_token])?, + None => tx.query(update_query, &[&email, &confirm_token, &recover_token, &account])?, }; match result.iter().next() { diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 2eca2452..39710fe1 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -128,6 +128,7 @@ pub enum RpcRequest { pub trait User { fn receive(&mut self, data: Vec, stripe: &StripeClient) -> Result; fn connected(&mut self) -> Result<(), Error>; + fn disconnected(&self) -> Result<(), Error>; fn send(&mut self, msg: RpcMessage) -> Result<(), Error>; } @@ -170,8 +171,8 @@ impl Handler for Connection { } fn on_close(&mut self, _: CloseCode, _: &str) { - info!("websocket disconnected id={:?}", self.id); self.events.send(Event::Disconnect(self.id)).unwrap(); + self.user.disconnected().unwrap(); } fn on_request(&mut self, req: &Request) -> ws::Result { @@ -198,7 +199,10 @@ impl Handler for Connection { if cookie.name() == TOKEN_HEADER { let db = self.pool.get().unwrap(); match account::from_token(&db, &cookie.value().to_string()) { - Ok(a) => self.user = Box::new(Authenticated::new(a, self.ws.clone(), self.events.clone(), self.pool.clone())), + Ok(a) => { + self.id = a.id; + self.user = Box::new(Authenticated::new(a, self.ws.clone(), self.events.clone(), self.pool.clone())); + }, Err(_) => return unauth(), } } diff --git a/server/src/user_anonymous.rs b/server/src/user_anonymous.rs index be39a052..fe1d9045 100644 --- a/server/src/user_anonymous.rs +++ b/server/src/user_anonymous.rs @@ -74,6 +74,10 @@ impl User for Anonymous { Ok(()) } + fn disconnected(&self) -> Result<(), Error> { + Ok(()) + } + fn receive(&mut self, data: Vec, _stripe: &StripeClient) -> Result { match from_slice::(&data) { Ok(v) => { diff --git a/server/src/user_authenticated.rs b/server/src/user_authenticated.rs index ce7581a6..eb617178 100644 --- a/server/src/user_authenticated.rs +++ b/server/src/user_authenticated.rs @@ -128,9 +128,9 @@ impl User for Authenticated { let wheel = account::chat_wheel(&db, a.id)?; self.ws.send(RpcMessage::ChatWheel(wheel))?; - if let Some(instance) = account::tutorial(&mut tx, &a)? { - self.ws.send(RpcMessage::InstanceState(instance))?; - } + // if let Some(instance) = account::tutorial(&mut tx, &a)? { + // self.ws.send(RpcMessage::InstanceState(instance))?; + // } // tx should do nothing tx.commit()?; @@ -138,6 +138,11 @@ impl User for Authenticated { Ok(()) } + fn disconnected(&self) -> Result<(), Error> { + info!("user disconnected account={:?}", self.account); + Ok(()) + } + fn receive(&mut self, data: Vec, stripe: &StripeClient) -> Result { // cast the msg to this type to receive method name let begin = Instant::now(); diff --git a/studios/package.json b/studios/package.json index 08c5353e..93382d32 100644 --- a/studios/package.json +++ b/studios/package.json @@ -1,6 +1,6 @@ { "name": "mnml-studios", - "version": "1.12.1", + "version": "1.12.2", "description": "", "main": "index.js", "scripts": {