diff --git a/vasl_templates/webapp/static/css/lfa.css b/vasl_templates/webapp/static/css/lfa.css
index 2b5ef70..6b0705e 100644
--- a/vasl_templates/webapp/static/css/lfa.css
+++ b/vasl_templates/webapp/static/css/lfa.css
@@ -63,7 +63,22 @@
border: 1px solid #ffc030 ; border-radius: 5px ; background: #fffcfc ;
padding: 2px 5px ;
}
-#lfa .hotness img.dice { position: absolute ; left: -10px ; top : -25px ; height: 50px ; }
+#lfa .hotness img.dice { position: absolute ; left: -10px ; top : -25px ; height: 50px ; cursor: pointer ; }
+
+/* hotness popup */
+#lfa .hotness-popup {
+ position: absolute ;
+ border: 1px solid #888 ; border-radius: 5px ;
+ padding: 0.5em ;
+ background: #f8f8f8 ;
+ z-index: 100 ;
+}
+#lfa .hotness-popup th { padding: 0.2em 0.5em 0 0.5em ; background: #ddd ; border: 1px dotted #ccc ; font-weight: normal ; }
+#lfa .hotness-popup td.player { white-space: nowrap ; }
+#lfa .hotness-popup .val { border: none ; text-align: center ; color: #444 ; }
+#lfa .hotness-popup td.icon { width: 50px ; }
+#lfa .hotness-popup img.die { height: 1.5em ; margin: 2px 0.2em 0 0 ; }
+#lfa .hotness-popup img.sniper { height: 1.75em ; margin-top: 3px ; }
/* options panel */
#lfa .options {
diff --git a/vasl_templates/webapp/static/images/sniper.png b/vasl_templates/webapp/static/images/sniper.png
new file mode 100644
index 0000000..fe7be95
Binary files /dev/null and b/vasl_templates/webapp/static/images/sniper.png differ
diff --git a/vasl_templates/webapp/static/lfa.js b/vasl_templates/webapp/static/lfa.js
index 7c0a21d..1a6a3a5 100644
--- a/vasl_templates/webapp/static/lfa.js
+++ b/vasl_templates/webapp/static/lfa.js
@@ -50,7 +50,7 @@ var gDistribDatasetPlayerIndex={}, gPieDatasetPlayerIndex={}, gTimePlotDatasetPl
var gDistribCharts={}, gPieCharts={}, gTimePlotChart, gHotnessChart ;
var $gDialog ;
-var $gBanner, $gHotness, $gSelectFilePopup, $gOptions, $gRollTypeDropList, $gStackBarGraphsCheckBox ;
+var $gBanner, $gHotness, $gHotnessPopup, $gSelectFilePopup, $gOptions, $gRollTypeDropList, $gStackBarGraphsCheckBox ;
var $gPlayerColorsButton, $gPlayerColorsPopup ;
var $gTimePlot, $gTimePlotChartWrapper ;
var $gTimePlotOptions, $gMovingAverageDropList, $gTimePlotZoomInButton, $gTimePlotZoomOutButton ;
@@ -77,11 +77,14 @@ SHORTCUT_HANDLERS = {
71: function () { // "G"
$gMovingAverageDropList.selectmenu("instance").button.focus() ;
},
- 88: function () { //"X"
+ 88: function () { // "X"
var $elem = $gBanner.find( ".select-file" ) ;
if ( $elem.css( "display" ) != "none" )
$gBanner.find( ".select-file" ).click() ;
},
+ 50: function() { // "2"
+ $( "#lfa .hotness img.dice" ).click() ;
+ },
} ;
gPrevSelectMenuKeyDownHandler = $.ui.selectmenu.prototype._buttonEvents.keydown ;
@@ -170,6 +173,8 @@ window.show_lfa_dialog = function( resp )
loadDialog() ;
},
close: function() {
+ // NOTE: We explicitly close everything so that they aren't visible next time we open.
+ closeAllPopupsAndDropLists() ;
// clean up handlers
gEventHandlers.cleanUp() ;
// clean up charts
@@ -195,6 +200,7 @@ function loadDialog()
// initialize
$gBanner = $( "#lfa .banner" ) ;
$gHotness = $( "#lfa .hotness" ).hide() ;
+ $gHotnessPopup = $( "#lfa .hotness-popup" ) ;
$gSelectFilePopup = $( "#lfa .select-file-popup" ) ;
$gOptions = $( "#lfa .options" ) ;
$gRollTypeDropList = $( "#lfa select[name='roll-type']" ) ;
@@ -217,6 +223,9 @@ function loadDialog()
gLogFileAnalysis = new LogFileAnalysis( gRawResponseData, -1 ) ;
var rollTypes = gLogFileAnalysis.getRollTypes() ;
+ // initialize the hotness popup
+ initHotnessPopup() ;
+
// initialize the player colors
var prevColorsLen = gUserSettings.lfa[ "player-colors" ].length ; // nb: this includes the "expected results" color
gLogFileAnalysis.forEachPlayer( function( playerId, playerNo ) {
@@ -343,6 +352,127 @@ function loadDialog()
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+function initHotnessPopup()
+{
+ function makeReport() {
+
+ // initialize
+ var rolls={}, snipers={} ;
+ gLogFileAnalysis.forEachPlayer( function( playerId, playerNo ) {
+ rolls[ playerId ] = {} ;
+ for ( var rollType in ROLL_TYPES )
+ rolls[ playerId ][ rollType ] = { 2: 0, 12: 0 } ;
+ snipers[ playerId ] = { 1: 0, 2: 0 } ;
+ } ) ;
+
+ // count how many 2's and 12's were rolled, and Sniper Activations
+ gLogFileAnalysis.extractEvents( 1, {
+ onRollEvent: function( evt ) {
+ var rollTotal = LogFileAnalysis.rollTotal( evt.rollValue ) ;
+ if ( evt.rollType == "SA" && ( rollTotal == 1 || rollTotal == 2 ) )
+ ++ snipers[ evt.playerId ][ rollTotal ] ;
+ else if ( ! LogFileAnalysis.isSingleDie( evt.rollValue ) && ( rollTotal == 2 || rollTotal == 12 ) )
+ ++ rolls[ evt.playerId ][ evt.rollType ][ rollTotal ] ;
+ }
+ } ) ;
+
+ // figure out which roll types had at least one 2 or 12
+ var rollTypesToShow = {} ;
+ gLogFileAnalysis.forEachPlayer( function( playerId, playerNo ) {
+ for ( var rollType in ROLL_TYPES ) {
+ if ( rolls[playerId][rollType][2] > 0 || rolls[playerId][rollType][12] > 0 )
+ rollTypesToShow[ rollType ] = true ;
+ }
+ } ) ;
+
+ // add the 2's and 12's to the report
+ var buf = [] ;
+ function addRollReport( tableClass, die1, die2 ) {
+ // add the header
+ buf.push( "
" ) ;
+ buf.push( "", "",
+ "",
+ ""
+ ) ;
+ for ( var rollType in ROLL_TYPES ) {
+ if ( rollTypesToShow[ rollType ] )
+ buf.push( " | ", rollType ) ;
+ }
+ gLogFileAnalysis.forEachPlayer( function( playerId, playerNo ) {
+ buf.push( " |
", "", makePlayerNameHTML(playerId) ) ;
+ for ( var rollType in ROLL_TYPES ) {
+ if ( ! rollTypesToShow[ rollType ] )
+ continue ;
+ var nRollTypes = rolls[ playerId ][ rollType ][ die1+die2 ] ;
+ buf.push( " | ", nRollTypes === 0 ? "-" : nRollTypes ) ;
+ }
+ } ) ;
+ buf.push( " |
" ) ;
+ }
+ addRollReport( "2s", 1, 1 ) ;
+ addRollReport( "12s", 6, 6 ) ;
+
+ // add a divider
+ buf.push(
+ "
",
+ "
"
+ ) ;
+
+ // add the Sniper Activations to the report
+ buf.push( "" ) ;
+ buf.push( "", "",
+ "",
+ " | ", "dr 1", " | ", "dr 2"
+ ) ;
+ gLogFileAnalysis.forEachPlayer( function( playerId, playerNo ) {
+ buf.push( " |
", "", makePlayerNameHTML(playerId) ) ;
+ [ 1, 2 ].forEach( function( val ) {
+ var nActivations = snipers[ playerId ][ val ] ;
+ buf.push( " | ", nActivations === 0 ? "-" : nActivations ) ;
+ } ) ;
+ } ) ;
+ buf.push( " |
" ) ;
+
+ // generate the report
+ return buf.join( "" ) ;
+ }
+
+ function makePlayerNameHTML( playerId ) {
+ return escapeHTML( gLogFileAnalysis.playerName( playerId ) ) ;
+ }
+
+ // add a click handler for the hotness popup
+ var $elem = $( "#lfa .hotness img.dice" ) ;
+ gEventHandlers.addHandler( $elem, "click", function( evt ) {
+ closeAllPopupsAndDropLists() ;
+ // NOTE: We have to re-generate the report each time it's shown, since the user
+ // may have chosen a different set of log files.
+ $gHotnessPopup.html( makeReport() ).show() ;
+ var maxWidth = 0 ;
+ $gHotnessPopup.find( "table" ).each( function() {
+ maxWidth = Math.max( $(this).outerWidth() , maxWidth ) ;
+ } ) ;
+ $gHotnessPopup.css( { width: maxWidth } ) ;
+ $gHotnessPopup.position( {
+ my: "right top", at: "left-5 top+2", of: $elem, collision: "fit"
+ } ) ;
+ stopEvent( evt ) ;
+ } ) ;
+
+ // handle clicks outside the popup (to dismiss it)
+ // NOTE: We do this by adding a click handler to the main dialog window, and a click handler
+ // to the popup that prevents the event from bubbling up i.e. if the main dialog window receives
+ // a click event, it must've been outside the popup window.
+ gEventHandlers.addHandler( $gHotnessPopup, "click", function() {
+ return false ;
+ } ) ;
+ gEventHandlers.addHandler( $("#lfa"), "click", function() {
+ $gHotnessPopup.hide() ;
+ } ) ;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
function initSelectFilePopup()
{
// initialize the file selection popup
@@ -1362,6 +1492,7 @@ function customTimePlotTooltip( tooltipModel )
var newLeft = position.left + window.pageXOffset + tooltipModel.caretX + marginX ;
if ( newLeft >= position.width - tooltipElem.offsetWidth - 20 )
newLeft = tooltipModel.caretX - tooltipElem.offsetWidth ;
+ tooltipElem.style["z-index"] = 150 ; // nb: put this on top of the hotness popup
tooltipElem.style.left = newLeft + "px" ;
tooltipElem.style.top = position.top + window.pageYOffset + tooltipModel.caretY - tooltipElem.offsetHeight - marginY + "px" ;
tooltipElem.style.background = tooltipModel.backgroundColor ;
@@ -1816,6 +1947,7 @@ function closeAllPopupsAndDropLists()
$(this).spectrum( "hide") ;
} ) ;
$gSelectFilePopup.hide() ;
+ $gHotnessPopup.hide() ;
// close all droplists
$( "#lfa select" ).each( function() {
diff --git a/vasl_templates/webapp/templates/lfa.html b/vasl_templates/webapp/templates/lfa.html
index 4343465..3f7d159 100644
--- a/vasl_templates/webapp/templates/lfa.html
+++ b/vasl_templates/webapp/templates/lfa.html
@@ -23,7 +23,7 @@
-
+
@@ -65,6 +65,8 @@
+
+
diff --git a/vasl_templates/webapp/tests/fixtures/analyze-vlog/hotness-report-1.vlog b/vasl_templates/webapp/tests/fixtures/analyze-vlog/hotness-report-1.vlog
new file mode 100644
index 0000000..acdf0f1
Binary files /dev/null and b/vasl_templates/webapp/tests/fixtures/analyze-vlog/hotness-report-1.vlog differ
diff --git a/vasl_templates/webapp/tests/fixtures/analyze-vlog/hotness-report-2.vlog b/vasl_templates/webapp/tests/fixtures/analyze-vlog/hotness-report-2.vlog
new file mode 100644
index 0000000..b32f946
Binary files /dev/null and b/vasl_templates/webapp/tests/fixtures/analyze-vlog/hotness-report-2.vlog differ
diff --git a/vasl_templates/webapp/tests/test_lfa.py b/vasl_templates/webapp/tests/test_lfa.py
index 4536445..c586648 100644
--- a/vasl_templates/webapp/tests/test_lfa.py
+++ b/vasl_templates/webapp/tests/test_lfa.py
@@ -232,16 +232,6 @@ def test_multiple_files( webapp, webdriver ):
assert player_names.pop() == "expected results"
assert player_names == expected
- def select_file( fname ):
- """Select one of the files being analyzed."""
- find_child( "#lfa .banner .select-file" ).click()
- popup = wait_for_elem( 2, "#lfa .select-file-popup" )
- for row in find_children( ".row", popup ):
- if find_child( "label", row ).text == fname:
- find_child( "input[type='radio']", row ).click()
- return
- assert False, "Couldn't find file: "+fname
-
def do_test(): #pylint: disable=missing-docstring
# NOTE: The "1a" and "1b" log files have the same players (Alice and Bob), but the "2" log file
@@ -325,7 +315,7 @@ def test_multiple_files( webapp, webdriver ):
check_color_pickers( [ "Alice", "Bob", "Chuck" ] )
# select a file and check the results
- select_file( "multiple-1a.vlog" )
+ _select_log_file( "multiple-1a.vlog" )
_select_roll_type( "" )
lfa = _get_chart_data( 1 )
assert lfa["timePlot"] == [
@@ -351,7 +341,7 @@ def test_multiple_files( webapp, webdriver ):
check_color_pickers( [ "Alice", "Bob" ] )
# select another file and check the results
- select_file( "multiple-2.vlog" )
+ _select_log_file( "multiple-2.vlog" )
_select_roll_type( "" )
lfa = _get_chart_data( 1 )
assert lfa["timePlot"] == [
@@ -376,7 +366,7 @@ def test_multiple_files( webapp, webdriver ):
check_color_pickers( [ "Bob", "Chuck" ] )
# select all files and check the results
- select_file( "All files" )
+ _select_log_file( "All files" )
_select_roll_type( "" )
check_all_files()
check_color_pickers( [ "Alice", "Bob", "Chuck" ] )
@@ -389,6 +379,91 @@ def test_multiple_files( webapp, webdriver ):
# ---------------------------------------------------------------------
+@pytest.mark.skipif( not pytest.config.option.vasl_mods, reason="--vasl-mods not specified" ) #pylint: disable=no-member
+@pytest.mark.skipif( not pytest.config.option.vassal, reason="--vassal not specified" ) #pylint: disable=no-member
+def test_hotness_report( webapp, webdriver ):
+ """Test generating the hotness popup."""
+
+ # initialize
+ control_tests = init_webapp( webapp, webdriver, vlog_persistence=1 )
+
+ def unload_report():
+ """Unload the hotness popup."""
+ find_child( "#lfa .hotness img.dice" ).click()
+ wait_for_elem( 2, "#lfa .hotness-popup" )
+ report = {}
+ for key in ( "2s", "12s", "snipers" ):
+ report[ key ] = unload_table(
+ "//div[@class='hotness-popup']//table[@class='{}']//tr".format( key )
+ )
+ return report
+
+ def do_test(): #pylint: disable=missing-docstring
+
+ # load the test log files
+ # vlog #1 vlog #2
+ # =============== ===============
+ # Alice SA 1 Alice TH 2
+ # Bob TC 2 Chuck Rally 2
+ # Chuck SA 2 Alice SA 2
+ # Bob Rally 12 Bob TH 2
+ # Bob SA 1 Chuck MC 12
+ # Chuck SA 1 Chuck CC 2
+ # Bob TC 12
+ # Chuck MC 2
+ # Chuck SA 1
+ _analyze_vlogs( [ "hotness-report-1.vlog", "hotness-report-2.vlog" ] )
+
+ # check the hotness popup
+ assert unload_report() == {
+ "2s": [
+ [ "MC", "Rally", "TH", "CC", "TC" ],
+ [ "Alice", "-", "-", "1", "-", "-" ],
+ [ "Bob", "-", "-", "1", "-", "1" ],
+ [ "Chuck", "1", "1", "-", "1", "-" ],
+ ],
+ "12s": [
+ [ "MC", "Rally", "TH", "CC", "TC" ],
+ [ "Alice", "-", "-", "-", "-", "-" ],
+ [ "Bob", "-", "1", "-", "-", "1" ],
+ [ "Chuck", "1", "-", "-", "-", "-" ],
+ ],
+ "snipers": [
+ [ "dr 1", "dr 2" ],
+ [ "Alice", "1", "1" ],
+ [ "Bob", "1", "-" ],
+ [ "Chuck", "2", "1" ],
+ ],
+ }
+
+ # select only one of the log files and check the hotness popup
+ _select_log_file( "hotness-report-2.vlog" )
+ assert unload_report() == {
+ "2s": [
+ [ "MC", "Rally", "TH", "CC" ],
+ [ "Alice", "-", "-", "1", "-" ],
+ [ "Bob", "-", "-", "1", "-" ],
+ [ "Chuck", "-", "1", "-", "1" ],
+ ],
+ "12s": [
+ [ "MC", "Rally", "TH", "CC" ],
+ [ "Alice", "-", "-", "-", "-" ],
+ [ "Bob", "-", "-", "-", "-" ],
+ [ "Chuck", "1", "-", "-", "-" ],
+ ],
+ "snipers": [
+ [ "dr 1", "dr 2" ],
+ [ "Alice", "-", "1" ],
+ [ "Bob", "-", "-" ],
+ [ "Chuck", "-", "-" ],
+ ],
+ }
+
+ # run the tests
+ run_vassal_tests( control_tests, do_test, False )
+
+# ---------------------------------------------------------------------
+
@pytest.mark.skipif( not pytest.config.option.vasl_mods, reason="--vasl-mods not specified" ) #pylint: disable=no-member
@pytest.mark.skipif( not pytest.config.option.vassal, reason="--vassal not specified" ) #pylint: disable=no-member
def test_3d6( webapp, webdriver ):
@@ -621,6 +696,16 @@ def _check_time_plot_values( expected_window_sizes, window_size, expected ):
vals = _unload_table( "time-plot" )
assert vals == expected
+def _select_log_file( fname ):
+ """Select one of the log files being analyzed."""
+ find_child( "#lfa .banner .select-file" ).click()
+ popup = wait_for_elem( 2, "#lfa .select-file-popup" )
+ for row in find_children( ".row", popup ):
+ if find_child( "label", row ).text == fname:
+ find_child( "input[type='radio']", row ).click()
+ return
+ assert False, "Couldn't find file: "+fname
+
def _unload_table( sel ):
"""Unload chart data from an HTML table."""
return unload_table(