diff --git a/credits.md b/credits.md index db7781a..5577303 100644 --- a/credits.md +++ b/credits.md @@ -1,6 +1,7 @@ WIP credits: * Banner image from https://benjaminhuen.blogspot.com/2010/12/spetch-dump.html - * Icon from https://opengameart.org/content/700-rpg-icons + * Icons from https://opengameart.org/content/700-rpg-icons * NordSudA font from https://fontlibrary.org/en/font/nord-sud * Theme modified from https://github.com/arulrajnet/attila + * Coloured icons from https://opengameart.org/content/colored-ability-icons diff --git a/negotiation.py b/negotiation.py index 8c18736..72c2400 100755 --- a/negotiation.py +++ b/negotiation.py @@ -70,7 +70,9 @@ def negotiation_create(): # are being sent to it instead. return flask.redirect('/negotiations/{}'.format(n)) nego = Negotiation(name = n, owner = user.id, taker_sway = 1, - market_sway = 6, is_first_negotiation = 1) + market_sway = 6, is_first_negotiation = True, + manual_negotiation = True, current_phase = 'marketprep', + ) pony.orm.commit() return flask.redirect('/negotiations/{}'.format(n)) @@ -91,6 +93,7 @@ def negotiation(negotiation): 'room_owner': nego.owner, 'room_owner_display_name': nego.owner.display_name if nego.owner.display_name else nego.owner.uid, 'participant_count': len(get_room_participants(negotiation)), + 'phase': get_negotiation_phase(nego), } r.data = render_page('negotiation.html', c) return r @@ -111,7 +114,147 @@ def handle_join_negotiation(json): room = json['room']) # Update the user's negotation state with what is stored on the server nego = get_negotiation(json['room']) - return nego.to_dict() + # Send the newly arrived use the computed phase information + phase = get_negotiation_phase(nego); + return {'negotiation': nego.to_dict(), 'phase': phase} + +phases = { + 'marketprep': { + 'previous': None, + 'next': 'takerprep', + 'display_name': 'Market Prep', + 'description': 'Set up the negotiation\'s basic information in the UI, under "Negotiation Settings". When you\'re done, click on the "Next" button (right hand swip image)', + 'description_taker': 'The Market is setting up the negotiation, hang on.', + 'has_sub_phases': False, + }, + 'takerprep': { + 'previous': 'marketprep', + 'next': 'negotiatorselection', + 'display_name': 'Taker Prep', + 'description': 'Each taker may take one of the prep actions: x, y, z', + 'has_sub_phases': False, + }, + 'negotiatorselection': { + 'previous': 'takerprep', + 'next': 'firstimpression', + 'display_name': 'Negotiator Selection', + 'description': 'The crew selectons one member to be the negotiator. This character will perform all of the negotiation actions, but may be supported by the rest of the crew during scams.', + 'has_sub_phases': False, + }, + 'firstimpression': { + 'previous': 'negotiatorselection', + 'next': 'negotiation', + 'display_name': 'First Impression', + 'description': 'The taker crew\'s negotiator rolls a leadership check. The results of this check determine the number of rounds that a negotiation lasts, the taker\'s starting position on the sway tracker, if the negotiation length is known to the takers, and who will lead in each negotiation round.', + 'has_sub_phases': False, + }, + 'negotiation': { + 'previous': 'firstimpression', + 'next': 'wrapup', + 'display_name': 'Negotiation', + 'description': 'A round robin of: both negotiators playing tactics in order, checking if the negotiation should end, and a scam if not all the remaining takers have done one already.', + 'has_sub_phases': True, + 'sub_phases_repeat': True, + 'sub_phases': [ + 'first_negotiator_tactic', + 'second_negotiatior_tactic', + 'check_negotiation_end', + 'scam', + ], + }, + 'wrapup': { + 'previous': 'negotiation', + 'next': 'done', + 'display_name': 'Wrap-Up', + 'description': 'Final details to close out the negotiation', + 'has_sub_phases': True, + 'sub_phases_repeat': False, + 'sub_phases': [ + 'leadership', + 'accept_backout', + 'undercut', + ], + }, + 'done': { + 'previous': 'wrapup', + 'next': None, + 'display_name': 'Done', + 'description': 'The negotiation is concluded', + 'has_sub_phases': False, + }, +} + +def get_negotiation_phase(nego): + r = dict(); + r['current_phase'] = nego.current_phase + r['current_sub_phase'] = nego.current_sub_phase + r['display_name'] = phases[r['current_phase']]['display_name'] if r['current_phase'] in phases else 'Unknown phase "{}"'.format(r['current_phase']) + r['description'] = phases[r['current_phase']]['description'] if r['current_phase'] in phases else 'Unknown phase "{}"'.format(r['current_phase']) + r['description_taker'] = phases[r['current_phase']]['description_taker'] if 'description_taker' in phases[r['current_phase']].keys() else r['description'] + # Can the takers see the round information? + r['visible_rounds'] = True + r['round_current'] = nego.negotiation_round + try: + r['round_max'] = max(floor(nego.first_impression_black / 2), 5) + except: + r['round_max'] = 'First impression not yet done' + r['previous_phase'] = phases[nego.current_phase]['previous'] + r['next_phase'] = phases[nego.current_phase]['next'] + return r + +@socketio.on('set next phase') +@pony.orm.db_session +def handle_set_next_phase(json): + r = flask.Response() + app.logger.info('Received set next phase request: {}'.format(json)) + uid, user = get_uid(r) + nego = get_negotiation(json['room']) + app.logger.info('Room owner is {}'.format(nego.owner.uid)) + if nego.owner.uid != uid: + # Refuse the update from non-owners + app.logger.warning('Refusing set next phase of {} from non-owner {}'.format(json['room'], uid)) + return False + current_phase = nego.current_phase + next_phase_key = phases[current_phase]['next']; + if next_phase_key is None: + # @TODO. Anything here? + return False + # @TODO Handle sub phases + nego.current_phase = next_phase_key + pony.orm.commit() + data = { + 'phase': get_negotiation_phase(nego), + 'transitioned_from': current_phase, + } + flask_socketio.emit('negotiation state changed', {**data}, room = json['room']) + return True + +@socketio.on('set previous phase') +@pony.orm.db_session +def handle_set_prev_phase(json): + r = flask.Response() + app.logger.info('Received set previous phase request: {}'.format(json)) + uid, user = get_uid(r) + nego = get_negotiation(json['room']) + app.logger.info('Room owner is {}'.format(nego.owner.uid)) + if nego.owner.uid != uid: + # Refuse the update from non-owners + app.logger.warning('Refusing set previous phase of {} from non-owner {}'.format(json['room'], uid)) + return False + current_phase = nego.current_phase + next_phase_key = phases[current_phase]['previous']; + if next_phase_key is None: + # @TODO. Anything here? + return False + # @TODO Handle sub phases + nego.current_phase = next_phase_key + pony.orm.commit() + data = { + 'phase': get_negotiation_phase(nego), + 'transitioned_from': current_phase, + } + flask_socketio.emit('negotiation state changed', {**data}, room = json['room']) + return True def get_room_participants(room): if '/' not in socketio.server.manager.get_namespaces(): @@ -253,6 +396,11 @@ if __name__ == '__main__': taker_crew_name = pony.orm.Optional(str) # Manual negotation manual_negotiation = pony.orm.Optional(bool) + # marketprep, takerprep, negotiatorselection, firstimpression, + # negotiation (first, second, checkendcondition, scam) + # wrapup ( leadership, accept_refuse, undercut) + current_phase = pony.orm.Optional(str) + current_sub_phase = pony.orm.Optional(str) # Pre-negotiation details takers = pony.orm.Optional(int) lead_negotiator = pony.orm.Optional(str) diff --git a/static/css/nordsud.css b/static/css/nordsud.css new file mode 100644 index 0000000..5115fe3 --- /dev/null +++ b/static/css/nordsud.css @@ -0,0 +1,21 @@ +/* Retrieved from https://fontlibrary.org/face/nord-sud */ +@font-face { + font-family: 'NordSudA'; + src: url('/static/font/NordSudA.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'NordSudB'; + src: url('/static/font/NordSudB.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'NordSudC'; + src: url('/static/font/NordSudC.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} diff --git a/static/css/style.css b/static/css/style.css index f652ea4..05a6e2a 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -973,8 +973,82 @@ img { max-width: 30%; } +.negotiation-sidebar p { + margin: 0; +} + +.negotiation-sidebar h4 { + margin-top: 0; + font-family: 'NordSudA'; + color: #600000; + text-shadow: 2px 4px #1d0202; + margin-top: 0.15em; + margin-bottom: 0.15em; +} + +.onoffswitch.yesno .onoffswitch-inner:before { + content: "YES"; +} + +.onoffswitch.yesno .onoffswitch-inner:after { + content: "NO"; +} + +.negotiation-sidebar .onoffswitch { + float: right; +} + +.negotiation-sidebar .negotiation-state-wrapper { + display: flex; + justify-content: space-between; +} + +.negotiation-sidebar h5 { + margin: 0; + font-family: 'NordSudA'; + color: #600000; + text-shadow: 1px 2px #1d0202; + text-decoration: underline; +} + +.fieldset .fieldset-button { + cursor: pointer; + height: 24px; + width: 24px; + font-size: 12px; + vertical-align: super; +} + +.fieldset.fieldset-open .fieldset-button:after { + content: "-Close-"; +} +.fieldset.fieldset-closed .fieldset-button:after { + content: "-Open-"; +} +.fieldset.fieldset-closed div:first-of-type { + display: none; +} + +@-webkit-keyframes glow { + to { + border-color: #69c800; + -webkit-box-shadow: 0 0 5px #69c800; + -moz-box-shadow: 0 0 5px #69c800; + box-shadow: 0 0 5px #69c800; + } +} + +.market-settings p.phase-description { + border: 1px solid transparent; + -webkit-animation: glow 1.0s infinite alternate; + -webkit-transition: border 1.0s linear, box-shadow 1.0s linear; + -moz-transition: border 1.0s linear, box-shadow 1.0s linear; + transition: border 1.0s linear, box-shadow 1.0s linear; +} + .negotiation-panel { min-width: 70%; + margin-left: 15px; } .negotiation-panel h3 { @@ -1974,6 +2048,9 @@ img { padding-left: 10px; background-color: #34A7C1; color: #FFFFFF; } +.onoffswitch-label[disabled] .onoffswitch-inner:before { + background-color: #2f3137; +} .onoffswitch-inner:after { content: "OFF"; padding-right: 10px; diff --git a/static/font/NordSudA.ttf b/static/font/NordSudA.ttf new file mode 100644 index 0000000..15e4bd0 Binary files /dev/null and b/static/font/NordSudA.ttf differ diff --git a/static/font/NordSudB.ttf b/static/font/NordSudB.ttf new file mode 100644 index 0000000..87baba2 Binary files /dev/null and b/static/font/NordSudB.ttf differ diff --git a/static/font/NordSudC.ttf b/static/font/NordSudC.ttf new file mode 100644 index 0000000..384c5a9 Binary files /dev/null and b/static/font/NordSudC.ttf differ diff --git a/static/images/button-next-disabled.png b/static/images/button-next-disabled.png new file mode 100644 index 0000000..c0020f2 Binary files /dev/null and b/static/images/button-next-disabled.png differ diff --git a/static/images/button-next.png b/static/images/button-next.png new file mode 100644 index 0000000..2d67516 Binary files /dev/null and b/static/images/button-next.png differ diff --git a/static/images/button-previous-disabled.png b/static/images/button-previous-disabled.png new file mode 100644 index 0000000..5ae6703 Binary files /dev/null and b/static/images/button-previous-disabled.png differ diff --git a/static/images/button-previous.png b/static/images/button-previous.png new file mode 100644 index 0000000..614ee95 Binary files /dev/null and b/static/images/button-previous.png differ diff --git a/templates/base.html b/templates/base.html index 1649cb8..1cec5c5 100644 --- a/templates/base.html +++ b/templates/base.html @@ -20,7 +20,7 @@ - + {% endblock head %} diff --git a/templates/negotiation.html b/templates/negotiation.html index 7598722..630d324 100644 --- a/templates/negotiation.html +++ b/templates/negotiation.html @@ -31,7 +31,8 @@ socket.on('connect', function() { console.log("Trying to join room " + room); socket.emit('join negotiation', {"room": room}, function(e) { - update_from_data(e); + update_from_data(e['negotiation']); + update_phase(e['phase']); }); }); window.addEventListener('beforeunload', function(e) { @@ -66,6 +67,9 @@ socket.on('negotiation updated', function(e) { update_from_data(e); }); + socket.on('negotiation state changed', function(e) { + update_phase(e['phase'], e['transitioned_from']); + }); function update_from_data(e) { console.log(e); @@ -91,13 +95,50 @@ $('#is-first-negotiation').prop('checked', e['is_first_negotiation'] == true) } } + + function update_phase(p, transition_from = null) { + console.log(p); + $('.phase-display-name').html(p['display_name']); + $('.phase-description').html(p['description']); + $('.phase-description-taker').html(p['description_taker']); + var was_disabled = $('#state-previous').attr('disabled'); + var previous_button = $('#state-previous'); + var next_button = $('#state-next'); + previous_button.attr('disabled', p['previous_phase'] == null); + next_button.attr('disabled', p['next_phase'] == null); + if (previous_button.attr('disabled')) { + previous_button.attr('src', '/static/images/button-previous-disabled.png'); + } + else { + previous_button.attr('src', '/static/images/button-previous.png'); + } + if (next_button.attr('disabled')) { + next_button.attr('src', '/static/images/button-next-disabled.png'); + } + else { + next_button.attr('src', '/static/images/button-next.png'); + } + } + + function phase_on_next() { + // Some client side validation when running in automatic should go here, before + // the signal is sent to the server. + console.log('next'); + socket.emit('set next phase', {"room": room}); + } + + function phase_on_prev() { + console.log('previous'); + socket.emit('set previous phase', {"room": room}); + } + function swayslot_on_dragstart(event) { - var ev = event.originalEvent; - ev.dataTransfer.setData("text/plain", ev.target.id); - let img = new Image(); - img.src = '/static/images/drag.png'; - ev.dataTransfer.setDragImage(img, 0, 0); - ev.dataTransfer.dropEffect = "move"; + var ev = event.originalEvent; + ev.dataTransfer.setData("text/plain", ev.target.id); + let img = new Image(); + img.src = '/static/images/drag.png'; + ev.dataTransfer.setDragImage(img, 0, 0); + ev.dataTransfer.dropEffect = "move"; } function swayslot_on_dragover(event) { event.preventDefault(); @@ -152,9 +193,23 @@ socket.emit('update negotiation', d); if (id == 'manual-negotiation') { $('#' + id).unbind('change').attr('disabled', true); + $('#' + id).siblings('.onoffswitch-label').attr('disabled', true); } } + function toggle_fieldset(e) { + console.log($(e.target)); + var parent = $(e.target).parents().parents('.fieldset').first(); + console.log(parent); + if (parent.hasClass('fieldset-open')) { + parent.removeClass('fieldset-open'); + parent.addClass('fieldset-closed'); + } + else { + parent.removeClass('fieldset-closed'); + parent.addClass('fieldset-open'); + } + } // Drag/drog for the swaytracker var inputChanges = {}; window.addEventListener('DOMContentLoaded', () => { @@ -174,8 +229,19 @@ }); } - // Disable the manual negotation toggle if it's already on. + $('#state-next').on('click', phase_on_next); + $('#state-previous').on('click', phase_on_prev); + + // Disable the manual negotiation toggle if it's already on. $('#manual-negotiation[checked]').unbind('change').attr('disabled', true); + $('#manual-negotiation[checked]').siblings('.onoffswitch-label').attr('disabled', true); + + // Add fieldset bindings + $('.fieldset .fieldset-button').on('click', function(e) { + toggle_fieldset(e); + }); + $('.fieldset.fieldset-open div').first().show(); + $('.fieldset:not(.fieldset-open) div').first().hide(); }); diff --git a/templates/partials/negotiation-sidebar.html b/templates/partials/negotiation-sidebar.html index e2f6995..04b918a 100644 --- a/templates/partials/negotiation-sidebar.html +++ b/templates/partials/negotiation-sidebar.html @@ -7,32 +7,65 @@ {% if user == room_owner.uid %}
+
+ +
+ {{ phase.display_name }} +
+
+ +
+

{{ phase.description }}

- Manual Negotiation -
- - +

+
+

Negotiation Settings

+
+ Manual Negotiation +
+ + +
+

+ Is First Negotiation? +
+ + +
+
+ + +
+ + +
+ + +
+ + +
+
-
- - -
- - -
- - -
- - -
- - -
- +
@@ -44,6 +77,13 @@
+{% else %} +
+

+ {{ phase.display_name }} +

+

{{ phase.description_taker }}

+
{% endif %}