@@ -133,19 +133,82 @@ const teleTowerOptions: TeleTowerResult[] = ['None', 'Failed', 'level1', 'level2
133133const driverQualityOptions : DriverQuality [ ] = [ 'great' , 'good' , 'ok' , 'rough' ] ;
134134const breakdownOptions : BreakdownType [ ] = [ 'None' , 'stuck' , 'tipped' , 'comms' , 'mechanism' , 'other' ] ;
135135
136- const foulLabels : Array < { key : keyof MatchData [ 'fouls' ] ; label : string } > = [
137- { key : 'pinning' , label : 'Pinning' } ,
138- { key : 'towerContactInEndgame' , label : 'Tower Contact (Endgame)' } ,
139- { key : 'outOfZoneShooting' , label : 'Out-of-Zone Shooting' } ,
140- { key : 'ejectedFuel' , label : 'Ejected Fuel' } ,
141- { key : 'other' , label : 'Other' } ,
136+ const foulLabels : Array < {
137+ key : keyof MatchData [ 'fouls' ] ;
138+ label : string ;
139+ definition : string ;
140+ example : string ;
141+ } > = [
142+ {
143+ key : 'pinning' ,
144+ label : 'Pinning' ,
145+ definition :
146+ 'Illegal restriction of an opponent robot’s movement by trapping it against a field element or another robot beyond the allowed time limit.' ,
147+ example : 'Holding a robot against the wall for more than 5 seconds.' ,
148+ } ,
149+ {
150+ key : 'towerContactInEndgame' ,
151+ label : 'Tower Contact (Endgame)' ,
152+ definition :
153+ 'Contacting or interfering with an opponent’s tower or climbing mechanism during the protected endgame period when such interactions are prohibited.' ,
154+ example : 'Hitting a robot while it is climbing in endgame.' ,
155+ } ,
156+ {
157+ key : 'outOfZoneShooting' ,
158+ label : 'Out-of-Zone Shooting' ,
159+ definition :
160+ 'Launching game pieces from a location on the field where scoring actions are not permitted by game rules.' ,
161+ example : 'Shooting while outside the designated scoring zone.' ,
162+ } ,
163+ {
164+ key : 'ejectedFuel' ,
165+ label : 'Ejected Fuel' ,
166+ definition :
167+ 'Intentionally or negligently expelling game pieces in a way that violates control, safety, or scoring rules.' ,
168+ example : 'Dumping balls across the field instead of scoring.' ,
169+ } ,
170+ {
171+ key : 'other' ,
172+ label : 'Other' ,
173+ definition :
174+ 'Any rule violation not explicitly categorized that results in a referee-assessed foul based on game rules.' ,
175+ example : 'Entering a protected zone illegally.' ,
176+ } ,
142177] ;
143178
144- const breakLabels : Array < { key : keyof MatchData [ 'breaks' ] ; label : string } > = [
145- { key : 'mechanism' , label : 'Mechanism' } ,
146- { key : 'battery' , label : 'Battery' } ,
147- { key : 'comms' , label : 'Comms' } ,
148- { key : 'bumper' , label : 'Bumper' } ,
179+ const breakLabels : Array < {
180+ key : keyof MatchData [ 'breaks' ] ;
181+ label : string ;
182+ definition : string ;
183+ example : string ;
184+ } > = [
185+ {
186+ key : 'mechanism' ,
187+ label : 'Mechanism' ,
188+ definition : 'Failure of a robot’s mechanical subsystem that impairs functionality.' ,
189+ example : 'Intake arm breaks and cannot collect game pieces.' ,
190+ } ,
191+ {
192+ key : 'battery' ,
193+ label : 'Battery' ,
194+ definition :
195+ 'Power loss or electrical failure due to battery issues such as depletion, disconnection, or voltage drop.' ,
196+ example : 'Robot shuts off mid-match.' ,
197+ } ,
198+ {
199+ key : 'comms' ,
200+ label : 'Comms' ,
201+ definition :
202+ 'Loss or degradation of communication between the robot and driver station, preventing control.' ,
203+ example : 'Robot stops responding due to radio disconnect.' ,
204+ } ,
205+ {
206+ key : 'bumper' ,
207+ label : 'Bumper' ,
208+ definition :
209+ 'Damage, detachment, or illegal configuration of bumpers that affects compliance or robot interaction.' ,
210+ example : 'Bumper falls off during defense.' ,
211+ } ,
149212] ;
150213
151214const autoFieldImageByAlliance : Record < AllianceColor , string > = {
@@ -349,6 +412,8 @@ function MatchApp() {
349412 const [ defenseReceived , setDefenseReceived ] = useState ( false ) ;
350413 const [ fouls , setFouls ] = useState < MatchData [ 'fouls' ] > ( makeEmptyFouls ( ) ) ;
351414 const [ breaks , setBreaks ] = useState < MatchData [ 'breaks' ] > ( makeEmptyBreaks ( ) ) ;
415+ const [ activeFoulInfo , setActiveFoulInfo ] = useState < keyof MatchData [ 'fouls' ] | null > ( null ) ;
416+ const [ activeBreakInfo , setActiveBreakInfo ] = useState < keyof MatchData [ 'breaks' ] | null > ( null ) ;
352417 const [ freeText , setFreeText ] = useState ( '' ) ;
353418 const [ actionTicks , setActionTicks ] = useState < ActionTick [ ] > ( [ ] ) ;
354419 const nextTickIdRef = useRef ( 1 ) ;
@@ -1478,25 +1543,48 @@ function MatchApp() {
14781543 { foulLabels . map ( entry => (
14791544 < div
14801545 key = { entry . key }
1481- className = 'flex items-center gap-2 rounded-lg border border-white/10 bg-[#121a28] px-2 py-2' >
1482- < HoldButton
1483- onHold = { ( ) => adjustFoul ( entry . key , - 1 ) }
1484- repeatDelay = { 120 }
1485- repeatInterval = { 90 }
1486- className = 'rounded bg-[#c44e4e] px-2 py-1 text-xs font-semibold text-white' >
1487- -
1488- </ HoldButton >
1489- < span className = 'flex-1 text-xs text-gray-200' > { entry . label } </ span >
1490- < span className = 'w-8 text-right text-sm font-semibold tabular-nums text-white' >
1491- { fouls [ entry . key ] }
1492- </ span >
1493- < HoldButton
1494- onHold = { ( ) => adjustFoul ( entry . key , 1 ) }
1495- repeatDelay = { 120 }
1496- repeatInterval = { 90 }
1497- className = 'rounded bg-[#48c55c] px-2 py-1 text-xs font-semibold text-black' >
1498- +
1499- </ HoldButton >
1546+ className = 'rounded-lg border border-white/10 bg-[#121a28]' >
1547+ < div className = 'flex items-center gap-2 px-2 py-2' >
1548+ < HoldButton
1549+ onHold = { ( ) => adjustFoul ( entry . key , - 1 ) }
1550+ repeatDelay = { 120 }
1551+ repeatInterval = { 90 }
1552+ className = 'rounded bg-[#c44e4e] px-2 py-1 text-xs font-semibold text-white' >
1553+ -
1554+ </ HoldButton >
1555+ < span className = 'flex-1 text-xs text-gray-200' > { entry . label } </ span >
1556+ < button
1557+ type = 'button'
1558+ onClick = { ( ) =>
1559+ setActiveFoulInfo ( previous =>
1560+ previous === entry . key ? null : entry . key
1561+ )
1562+ }
1563+ aria-expanded = { activeFoulInfo === entry . key }
1564+ className = { `rounded-full border px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide transition ${
1565+ activeFoulInfo === entry . key
1566+ ? 'border-sky-300/70 bg-sky-300/15 text-sky-100'
1567+ : 'border-white/25 bg-white/5 text-gray-300 hover:bg-white/10'
1568+ } `} >
1569+ Info
1570+ </ button >
1571+ < span className = 'w-8 text-right text-sm font-semibold tabular-nums text-white' >
1572+ { fouls [ entry . key ] }
1573+ </ span >
1574+ < HoldButton
1575+ onHold = { ( ) => adjustFoul ( entry . key , 1 ) }
1576+ repeatDelay = { 120 }
1577+ repeatInterval = { 90 }
1578+ className = 'rounded bg-[#48c55c] px-2 py-1 text-xs font-semibold text-black' >
1579+ +
1580+ </ HoldButton >
1581+ </ div >
1582+ { activeFoulInfo === entry . key ? (
1583+ < div className = 'border-t border-white/10 bg-[#0f1522] px-3 py-2 text-[11px] text-gray-200' >
1584+ < p > { entry . definition } </ p >
1585+ < p className = 'mt-1 text-gray-300' > Example: { entry . example } </ p >
1586+ </ div >
1587+ ) : null }
15001588 </ div >
15011589 ) ) }
15021590 </ div >
@@ -1510,25 +1598,48 @@ function MatchApp() {
15101598 { breakLabels . map ( entry => (
15111599 < div
15121600 key = { entry . key }
1513- className = 'flex items-center gap-2 rounded-lg border border-white/10 bg-[#121a28] px-2 py-2' >
1514- < HoldButton
1515- onHold = { ( ) => adjustBreak ( entry . key , - 1 ) }
1516- repeatDelay = { 120 }
1517- repeatInterval = { 90 }
1518- className = 'rounded bg-[#c44e4e] px-2 py-1 text-xs font-semibold text-white' >
1519- -
1520- </ HoldButton >
1521- < span className = 'flex-1 text-xs text-gray-200' > { entry . label } </ span >
1522- < span className = 'w-8 text-right text-sm font-semibold tabular-nums text-white' >
1523- { breaks [ entry . key ] }
1524- </ span >
1525- < HoldButton
1526- onHold = { ( ) => adjustBreak ( entry . key , 1 ) }
1527- repeatDelay = { 120 }
1528- repeatInterval = { 90 }
1529- className = 'rounded bg-[#48c55c] px-2 py-1 text-xs font-semibold text-black' >
1530- +
1531- </ HoldButton >
1601+ className = 'rounded-lg border border-white/10 bg-[#121a28]' >
1602+ < div className = 'flex items-center gap-2 px-2 py-2' >
1603+ < HoldButton
1604+ onHold = { ( ) => adjustBreak ( entry . key , - 1 ) }
1605+ repeatDelay = { 120 }
1606+ repeatInterval = { 90 }
1607+ className = 'rounded bg-[#c44e4e] px-2 py-1 text-xs font-semibold text-white' >
1608+ -
1609+ </ HoldButton >
1610+ < span className = 'flex-1 text-xs text-gray-200' > { entry . label } </ span >
1611+ < button
1612+ type = 'button'
1613+ onClick = { ( ) =>
1614+ setActiveBreakInfo ( previous =>
1615+ previous === entry . key ? null : entry . key
1616+ )
1617+ }
1618+ aria-expanded = { activeBreakInfo === entry . key }
1619+ className = { `rounded-full border px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide transition ${
1620+ activeBreakInfo === entry . key
1621+ ? 'border-sky-300/70 bg-sky-300/15 text-sky-100'
1622+ : 'border-white/25 bg-white/5 text-gray-300 hover:bg-white/10'
1623+ } `} >
1624+ Info
1625+ </ button >
1626+ < span className = 'w-8 text-right text-sm font-semibold tabular-nums text-white' >
1627+ { breaks [ entry . key ] }
1628+ </ span >
1629+ < HoldButton
1630+ onHold = { ( ) => adjustBreak ( entry . key , 1 ) }
1631+ repeatDelay = { 120 }
1632+ repeatInterval = { 90 }
1633+ className = 'rounded bg-[#48c55c] px-2 py-1 text-xs font-semibold text-black' >
1634+ +
1635+ </ HoldButton >
1636+ </ div >
1637+ { activeBreakInfo === entry . key ? (
1638+ < div className = 'border-t border-white/10 bg-[#0f1522] px-3 py-2 text-[11px] text-gray-200' >
1639+ < p > { entry . definition } </ p >
1640+ < p className = 'mt-1 text-gray-300' > Example: { entry . example } </ p >
1641+ </ div >
1642+ ) : null }
15321643 </ div >
15331644 ) ) }
15341645 </ div >
0 commit comments