diff --git a/html/components/pbt-input.js b/html/components/pbt-input.js index 8257b7b1..cd1417bd 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 f91ace0c..3b830650 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 0b71870e..53aae756 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 d254c4f9..2ddb2644 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 e86c9ec5..c47c8074 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); }