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(