From e144773085867fbe7fbbc87f131ac8956404d46b Mon Sep 17 00:00:00 2001 From: Katrin Bratland <115959+katpet@users.noreply.github.com> Date: Wed, 4 Oct 2023 19:36:03 -0700 Subject: [PATCH] Box clocks started when jam isn't running now go into paused mode and wait for jam start * Addressing user feedback, changed behavior of clocks started between jams. Previously, they would start counting and it was the responsibility of the user to reset them before the next jam. Now, when clocks are started between jams they don't start counting yet. Instead the clock is indicated to be active by changing the color from gray to white and disabling the start button. They look the same way as when paused between jams. When the jam starts the clock will start counting down. This accomadates the scenario where a skater sits in the box between jams; the penalty timer can go ahead and hit start for them, and it'll get their clock ready to start at the next jam. * When penalty box clocks are paused at the end of a jam, they now stay white and the start button is dimmed, to make it more clear that they're still active and will resume again at the next jam start. * Fixed scenario where both jammers are put in box between jams. Their timers will set to zero, or if they have different numbers of penalties those will reduce as far as possible, so it's clear how much time they will have on the clock when the jam starts. * Fixed bug where adding extra time to a clock before starting wasn't working; it would get reset back to default penalty time at start. * Fixed some bugs in jammer logic. If jammers have multiple penalties we now cancel extra unserved penalties as much as possible. --- html/components/pbt-input.js | 110 +++++++++--- .../scoreboard/core/game/BoxSeatImpl.java | 161 +++++++++++------- .../scoreboard/core/game/FieldingImpl.java | 4 - .../scoreboard/core/interfaces/BoxSeat.java | 1 + .../scoreboard/core/interfaces/Fielding.java | 1 - 5 files changed, 189 insertions(+), 88 deletions(-) diff --git a/html/components/pbt-input.js b/html/components/pbt-input.js index 8257b7b1b..cd1417bd1 100644 --- a/html/components/pbt-input.js +++ b/html/components/pbt-input.js @@ -20,6 +20,15 @@ const TimerControl = { Reset: "Reset" } +const Colors = { + Paused: "white", + Alert: "crimson", + Stop: "crimson", + Inactive: "dimgrey", + Active: "white", + Start: "limegreen" +} + function isTrue(value) { 'use strict'; if (typeof value === 'boolean') { @@ -295,39 +304,98 @@ function setupCallbacks() { ); WS.Register( - 'ScoreBoard.CurrentGame.BoxClock(*).Time', + [ + 'ScoreBoard.CurrentGame.BoxClock(*).Time', + 'ScoreBoard.CurrentGame.Team(*).BoxSeat(*).Started', + 'ScoreBoard.CurrentGame.Clock(Jam).Running', + ], function (k, v) { - var running = isTrue(WS.state['ScoreBoard.CurrentGame.BoxClock(' + k.BoxClock + ').Running']); - if(running) { + var running = isTrue(WS.state['ScoreBoard.CurrentGame.BoxClock(' + k.BoxClock + ').Running']); + if(running) { if(v <= 10000) { - $('.Time.'+k.BoxClock).css('color', 'crimson'); + $('.Time.'+k.BoxClock).css('color', Colors.Alert); } else { - $('.Time.'+k.BoxClock).css('color', 'white'); + $('.Time.'+k.BoxClock).css('color', Colors.Active); + } + } else if(k.BoxClock) { + var jamrunning = isTrue(WS.state['ScoreBoard.CurrentGame.Clock(Jam).Running']); + var team = k.BoxClock.startsWith('Team2') ? 2 : 1; + var pos = ''; + for(const p of ["Jammer", "Blocker1", "Blocker2", "Blocker3"]) { + if(k.BoxClock.endsWith(p)) { + pos = p; + } + } + + var started = isTrue(WS.state['ScoreBoard.CurrentGame.Team('+team+').BoxSeat('+ pos+').Started']); + if(!jamrunning && started) { + // indicate a clock that is paused between jams so it's clearer it'll start back up on next jam + $('.Time.'+k.BoxClock).css('color', Colors.Paused); + } + } else if(k.BoxSeat) { + if(v) { + $('.Time.Team' + k.Team + k.BoxSeat).css('color', Colors.Paused); + var jamrunning = isTrue(WS.state['ScoreBoard.CurrentGame.Clock(Jam).Running']); + if(!jamrunning) { + $('.BoxTimerPage button.Team'+k.Team + k.BoxSeat+'.Start').prop("disabled", true); + } + } else { + $('.Time.Team' + k.Team + k.BoxSeat).css('color', Colors.Inactive); + $('.BoxTimerPage button.Team'+k.Team + k.BoxSeat+'.Start').prop("disabled", false); } - } else { - $('.Time.'+k.BoxClock).css('color', 'gray'); } } ); WS.Register( - 'ScoreBoard.CurrentGame.BoxClock(*).Running', + [ + 'ScoreBoard.CurrentGame.BoxClock(*).Running', + 'ScoreBoard.CurrentGame.Clock(Jam).Running', + 'ScoreBoard.CurrentGame.Team(*).BoxSeat(*).Started', + ], function (k, v) { - if(isTrue(v)) { + var running = isTrue(WS.state['ScoreBoard.CurrentGame.BoxClock(' + k.BoxClock + ').Running']); + if(k.BoxClock == null) { + return; + } + if(running) { $('.BoxTimerPage button.'+k.BoxClock+'.Stop').show(); $('.BoxTimerPage button.'+k.BoxClock+'.Start').hide(); $('.BoxTimerPage button.'+k.BoxClock+'.Reset').prop("disabled", true); - $('.Time.'+k.BoxClock).css('color', 'white'); + $('.Time.'+k.BoxClock).css('color', Colors.Active); } else { $('.BoxTimerPage button.'+k.BoxClock+'.Stop').hide(); $('.BoxTimerPage button.'+k.BoxClock+'.Start').show(); + $('.BoxTimerPage button.'+k.BoxClock+'.Start').prop("disabled", false); $('.BoxTimerPage button.'+k.BoxClock+'.Reset').prop("disabled", false); - $('.Time.'+k.BoxClock).css('color', 'dimgray'); - } + + var jamrunning = isTrue(WS.state['ScoreBoard.CurrentGame.Clock(Jam).Running']); + var team = k.BoxClock.startsWith('Team2') ? 2 : 1; + var pos = ''; + + for(const p of ["Jammer", "Blocker1", "Blocker2", "Blocker3"]) { + if(k.BoxClock.endsWith(p)) { + pos = p; + } + } + var started = isTrue(WS.state['ScoreBoard.CurrentGame.Team('+team+').BoxSeat('+pos+').Started']); + if(started) { + $('.Time.'+k.BoxClock).css('color', Colors.Paused); + if(!jamrunning) { + $('.BoxTimerPage button.'+k.BoxClock+'.Start').prop("disabled", true); + } + } else { + $('.Time.'+k.BoxClock).css('color', Colors.Inactive); + $('.BoxTimerPage button.'+k.BoxClock+'.Start').prop("disabled", false); + } + } } ); - WS.Register('ScoreBoard.CurrentGame.Team(*).Skater(*).Role', + WS.Register( + [ + 'ScoreBoard.CurrentGame.Team(*).Skater(*).Role', + ], function (k, v) { if(v == Position.Jammer) { var selectId = $('#PenaltyBoxJammersTeam'+k.Team+'DisplayJammer,#PenaltyBoxBothTeamsTeam'+k.Team+'DisplayJammer'); @@ -341,7 +409,7 @@ function setupCallbacks() { WS.Register( [ 'ScoreBoard.CurrentGame.Team(*).BoxSeat(*).BoxSkaterPenalties', - ], + ], function (k, v) { var sel = $('.Team'+k.Team+'.'+k.BoxSeat+'.PenaltyCount'); if(sel.length) { @@ -360,7 +428,7 @@ WS.Register([ function(k, v) { // Edit the popup that allows adding/removing time to match penalty duration rule var sel = $('.editTime'); - if(sel.length) { + if(sel.length && v) { // Convert from format like 0:30 or 1:00 to seconds, // which appears in menu like '+30', '-30' or '+60', '-60' @@ -437,12 +505,12 @@ WS.Register( } function setupButtonsStyle() { - $('.BoxTimerPage button.Start').css('background-color', 'limegreen'); - $('.BoxTimerPage button.Start').css('color', 'white'); - $('.BoxTimerPage button.Stop').css('background-color', 'crimson'); - $('.BoxTimerPage button.Stop').css('color', 'white'); - $('.BoxTimerPage button.Reset').css('background-color', 'dimgray'); - $('.BoxTimerPage button.Reset').css('color', 'white'); + $('.BoxTimerPage button.Start').css('background-color', Colors.Start); + $('.BoxTimerPage button.Start').css('color', Colors.Active); + $('.BoxTimerPage button.Stop').css('background-color', Colors.Stop); + $('.BoxTimerPage button.Stop').css('color', Colors.Active); + $('.BoxTimerPage button.Reset').css('background-color', Colors.Inactive); + $('.BoxTimerPage button.Reset').css('color', Colors.Active); } function setupEditButton(pageId, teamId, pos, seatId) { diff --git a/src/com/carolinarollergirls/scoreboard/core/game/BoxSeatImpl.java b/src/com/carolinarollergirls/scoreboard/core/game/BoxSeatImpl.java index f91ace0c5..3b8306501 100644 --- a/src/com/carolinarollergirls/scoreboard/core/game/BoxSeatImpl.java +++ b/src/com/carolinarollergirls/scoreboard/core/game/BoxSeatImpl.java @@ -75,7 +75,11 @@ public void setSkater(Skater s) { @Override public void startBox() { + Clock jc = team.getGame().getClock(Clock.ID_JAM); if(started()) { + if(!jc.isRunning()) { + return; + } // Resume a clock that was paused. synchronized (coreLock) { Clock pc = getBoxClock(); @@ -109,6 +113,92 @@ public void startBox() { } } restartBox(); + if(!jc.isRunning()) { + stopBox(); // If jam not running, pause right away + getBoxClock().resetTime(); + } + } + } + + private void doJammerLogic(Clock pc) { + if(getFloorPosition() != FloorPosition.JAMMER) { + return; + } + Team otherTeam = (team.getProviderId() == Team.ID_1) ? team.getGame().getTeam(Team.ID_2) : team.getGame().getTeam(Team.ID_1); + for(BoxSeat bs : otherTeam.getAll(Team.BOX_SEAT)) { + if(bs.getFloorPosition() == FloorPosition.JAMMER) { + Clock otherClock = bs.getBoxClock(); + if(otherClock != null && (otherClock.isRunning() || bs.started())) { + long penaltyDuration = team.getGame().getLong(Rule.PENALTIES_DURATION); + long otherMaximum = otherClock.getMaximumTime(); + long thisMaximum = pc.getMaximumTime(); + long otherTime = otherClock.getTime(); + long thisTime = pc.getTime(); + long otherElapsed2 = otherMaximum - otherTime; + long epsilon = 800; + if(otherElapsed2 < epsilon) { + // This value can end up being not quite zero or even a bit negative, which is undesirable + // because then it'll be ignored when we try to set the clock value, so round to 0 when approximately 0. + // But, use a small number of milliseconds here to represent zero, because + // that will trigger the UI to notice that the clock has changed; it'll still show as 0 seconds. + //otherElapsed2 = 1; + } + long otherNewTime = otherTime; + long thisNewTime = thisMaximum; + + if(otherNewTime >= penaltyDuration || thisNewTime >= penaltyDuration) { + // A jammer has multiple penalties. + // Cancel extra penalties as far as we can, taking into account how much the other jammer has already served. + // Since these times aren't exact, check within some small amount of penalty length. + while(otherNewTime >= (penaltyDuration - epsilon) && thisNewTime >= (penaltyDuration - epsilon)) { + otherNewTime -= penaltyDuration; + thisNewTime -= penaltyDuration; + otherMaximum -= penaltyDuration; + thisMaximum -= penaltyDuration; + } + } + + if(otherMaximum == 0 || thisMaximum == 0) { + // If a max reduced to zero above, the values for thisNewTime and otherNewTime are already good + } else if(otherMaximum == thisMaximum) { + // Simple, common case. + // Sitting jammer is released, + // and arriving jammer serves as much as sitting jammer did. + thisNewTime = otherElapsed2; + otherNewTime = 1; + } else if(otherMaximum > penaltyDuration && thisMaximum == penaltyDuration) { + // This and next case are similar to the simple one above except the max times differ, + // so need to account for that in the elapsed time. + thisNewTime = otherElapsed2 - (otherMaximum - thisMaximum); + otherNewTime = 1; + } else if(otherMaximum == penaltyDuration && thisMaximum > penaltyDuration) { + thisNewTime = otherElapsed2 + (thisMaximum - otherMaximum); + otherNewTime = 1; + } else { + // The nightmare scenario! + // A jammer has returned to the box while + // sitting jammer is already serving a reduced single penalty, + // so can't reduce that any more. + // Sitting jammer must finish their penalty, + // and arriving jammer gets regular penalty. + otherNewTime = otherTime; + thisNewTime = penaltyDuration; + } + + // jigger refresh of clock + if(thisNewTime == 0) { + thisNewTime = epsilon; + } + if(thisNewTime == thisTime) { + thisNewTime += 1; + } + pc.setMaximumTime(thisNewTime); + pc.setTime(thisNewTime); + otherClock.setTime(otherNewTime); + pc.restart(); + } + break; + } } } @@ -119,68 +209,11 @@ private void restartBox() { if(pc == null) { return; } - pc.setMaximumTime(team.getGame().getLong(Rule.PENALTIES_DURATION)); pc.setCountDirectionDown(true); pc.restart(); setWallStartTime(ScoreBoardClock.getInstance().getCurrentWalltime()); if(hasFloorPosition()) { - if(getFloorPosition() == FloorPosition.JAMMER) { - // Jammer logic - Team otherTeam = (team.getProviderId() == Team.ID_1) ? team.getGame().getTeam(Team.ID_2) : team.getGame().getTeam(Team.ID_1); - for(BoxSeat bs : otherTeam.getAll(Team.BOX_SEAT)) { - if(bs.getFloorPosition() == FloorPosition.JAMMER) { - Clock otherClock = bs.getBoxClock(); - if(otherClock != null && otherClock.isRunning()) { - long penaltyDuration = team.getGame().getLong(Rule.PENALTIES_DURATION); - long otherMaximum = otherClock.getMaximumTime(); - long otherTime = otherClock.getTime(); - long otherElapsed2 = otherMaximum - otherTime; - long otherNewTime = 0; - long thisNewTime = 0; - if(otherMaximum == penaltyDuration) { - // Simple, common case. - // Sitting jammer is released, - // and arriving jammer serves as much as sitting jammer did. - thisNewTime = otherElapsed2; - otherNewTime = 0; - } else if(otherMaximum > penaltyDuration) { - if(otherTime > penaltyDuration) { - // In this case sitting jammer has multiple penalties. - // Turn new jammer away, - // and reduce sitting jammer by one penalty. - // If sitting jammer happens to have even more - // than two penalties, other jammer can come and go - // reducing sitting jammer by one penalty each time - // until we arrive at the simple case. - thisNewTime = 1000; // to cause zero to appear - otherNewTime = otherTime - penaltyDuration; - } else { - // Reverts to simple case - // Sitting jammer leaves, - // New jammer sits for the amount that - // other jammer served of their second penalty. - thisNewTime = otherElapsed2 - penaltyDuration; - otherNewTime = 0; - } - } else { - // The nightmare scenario! - // A jammer has returned to the box while - // sitting jammer is already serving a reduced single penalty, - // so can't reduce that any more. - // Sitting jammer must finish their penalty, - // and arriving jammer gets regular penalty. - otherNewTime = otherTime; - thisNewTime = penaltyDuration; - } - pc.setMaximumTime(thisNewTime); - pc.setTime(thisNewTime); - pc.restart(); - otherClock.setTime(otherNewTime); - } - break; - } - } - } + doJammerLogic(pc); Fielding f = team.getPosition(getFloorPosition()).getCurrentFielding(); if( f != null ) { BoxTrip bt = f.getCurrentBoxTrip(); @@ -201,7 +234,7 @@ public void resetBox() { long penaltyDuration = team.getGame().getLong(Rule.PENALTIES_DURATION); pc.setMaximumTime(penaltyDuration); } - wallStartTime = 0; + setWallStartTime(0); resetSkater(); } @@ -256,10 +289,11 @@ public void endBox() { s.setPenaltyCount(2, penaltyCountP2 + getCurNumPenalties()); } } - //curTrip.end(); fpValid = false; numPenalties = 1; - f.execute(Fielding.END_BOX_TRIP, Source.OTHER); + if(f.getCurrentBoxTrip() != null) { + f.getCurrentBoxTrip().end(); + } } } } @@ -291,7 +325,9 @@ public void setBoxSkater(String number) { if(cur_s != null) { Fielding cur_f = cur_s.getCurrentFielding(); if(cur_f != null) { - cur_f.getCurrentBoxTrip().delete(); + if(cur_f.getCurrentBoxTrip() != null) { + cur_f.getCurrentBoxTrip().delete(); + } } } } @@ -368,6 +404,7 @@ public FloorPosition getFloorPosition() { private void setWallStartTime(long w) { wallStartTime = w; + set(STARTED, wallStartTime != 0); } private long getWallStartTime() { diff --git a/src/com/carolinarollergirls/scoreboard/core/game/FieldingImpl.java b/src/com/carolinarollergirls/scoreboard/core/game/FieldingImpl.java index 0b71870e7..53aae7562 100644 --- a/src/com/carolinarollergirls/scoreboard/core/game/FieldingImpl.java +++ b/src/com/carolinarollergirls/scoreboard/core/game/FieldingImpl.java @@ -124,10 +124,6 @@ public void execute(Command prop, Source source) { if (prop == UNEND_BOX_TRIP && getCurrentBoxTrip() != null && !getCurrentBoxTrip().isCurrent()) { getCurrentBoxTrip().unend(); } - if (prop == END_BOX_TRIP && getCurrentBoxTrip() != null) { - getCurrentBoxTrip().end(); - itemRemoved(BOX_TRIP, getCurrentBoxTrip(), source); - } } @Override diff --git a/src/com/carolinarollergirls/scoreboard/core/interfaces/BoxSeat.java b/src/com/carolinarollergirls/scoreboard/core/interfaces/BoxSeat.java index d254c4f92..2ddb2644d 100644 --- a/src/com/carolinarollergirls/scoreboard/core/interfaces/BoxSeat.java +++ b/src/com/carolinarollergirls/scoreboard/core/interfaces/BoxSeat.java @@ -27,6 +27,7 @@ public interface BoxSeat extends ScoreBoardEventProvider { public static final Value BOX_SKATER = new Value<>(String.class, "BoxSkater", "", props); public static final Value BOX_TIME_CHANGE = new Value<>(Integer.class, "BoxTimeChange", 0, props); public static final Value BOX_SKATER_PENALTIES = new Value<>(Integer.class, "BoxSkaterPenalties", 0, props); + public static final Value STARTED = new Value<>(Boolean.class, "Started", false, props); public static final Command START_BOX = new Command("StartBox", props); public static final Command STOP_BOX = new Command("StopBox", props); diff --git a/src/com/carolinarollergirls/scoreboard/core/interfaces/Fielding.java b/src/com/carolinarollergirls/scoreboard/core/interfaces/Fielding.java index e86c9ec58..c47c8074e 100644 --- a/src/com/carolinarollergirls/scoreboard/core/interfaces/Fielding.java +++ b/src/com/carolinarollergirls/scoreboard/core/interfaces/Fielding.java @@ -44,5 +44,4 @@ public interface Fielding extends ParentOrderedScoreBoardEventProvider public static final Command ADD_BOX_TRIP = new Command("AddBoxTrip", props); public static final Command UNEND_BOX_TRIP = new Command("UnendBoxTrip", props); - public static final Command END_BOX_TRIP = new Command("EndBoxTrip", props); }