Add the start of phase/state synchronization and transitions

This commit is contained in:
kienan 2020-04-23 00:25:49 -04:00
parent 774434b4ad
commit 74330b9af2
14 changed files with 389 additions and 36 deletions

View File

@ -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

View File

@ -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)

21
static/css/nordsud.css Normal file
View File

@ -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;
}

View File

@ -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;

BIN
static/font/NordSudA.ttf Normal file

Binary file not shown.

BIN
static/font/NordSudB.ttf Normal file

Binary file not shown.

BIN
static/font/NordSudC.ttf Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -20,7 +20,7 @@
<!-- Custom fonts -->
<!-- link href='https://fonts.googleapis.com/css?family=Montserrat:400,300' rel='stylesheet' type='text/css' />
<link href="https://fonts.googleapis.com/css?family=Lato" rel="stylesheet" type="text/css" /> -->
<link rel="stylesheet" media="screen" href="https://fontlibrary.org/face/nord-sud" type="text/css"/>
<link rel="stylesheet" media="screen" href="/static/css/nordsud.css" type="text/css"/>
{% endblock head %}
</head>
<body class="home-template " {% block bodyclasses %}{% endblock bodyclasses %}>

View File

@ -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();
});
</script>

View File

@ -7,32 +7,65 @@
{% if user == room_owner.uid %}
<div class="market-settings">
<div class="negotiation-state-wrapper">
<input id="state-previous" type="image" width="32" height="32" alt="Go the previous state"
{% if phase.current_phase == 'marketprep' %}
disabled="true"
src="/static/images/button-previous-disabled.png"
{% else %}
src="/static/images/button-previous.png"
{% endif %}>
<h5>
<span class="phase-display-name">{{ phase.display_name }}</span>
</h5>
<br>
<input id="state-next" type="image" width="32" height="32" alt="Next"
{% if phase.current_phase == 'done' %}
disabled="true"
src="/static/images/button-next-disabled.png"
{% else %}
src="/static/images/button-next.png"
{% endif %}>
</div>
<p class="phase-description">{{ phase.description }}</p>
<form id="market-settings">
<span>Manual Negotiation</span>
<div class="onoffswitch">
<input id="manual-negotiation" type="checkbox" class="onoffswitch-checkbox" {% if negotiation.manual_negotiation %}checked{% endif %}>
<label for="manual-negotiation" class="onoffswitch-label">
<span class="onoffswitch-inner"></span>
<span class="onoffswitch-switch"></span>
</label>
<p class="phase-description-market"></p>
<div class="fieldset {% if phase.current_phase == 'marketprep' %} fieldset-open{% else %} fieldset-closed{% endif %}">
<h4>Negotiation Settings<span data-target=".negotiation-settings-wrapper" class="fieldset-button"></span></h4>
<div class="negotiation-settings-wrapper">
<span>Manual Negotiation</span>
<div class="onoffswitch">
<input id="manual-negotiation" type="checkbox" class="onoffswitch-checkbox" {% if negotiation.manual_negotiation %}checked{% endif %}>
<label for="manual-negotiation" class="onoffswitch-label">
<span class="onoffswitch-inner"></span>
<span class="onoffswitch-switch"></span>
</label>
</div>
<br><br>
<span>Is First Negotiation?</span>
<div class="onoffswitch yesno">
<input id="is-first-negotiation" type="checkbox" class="onoffswitch-checkbox" {% if negotiation.is_first_negotiation %}checked{% endif %}>
<label for="is-first-negotiation" class="onoffswitch-label">
<span class="onoffswitch-inner"></span>
<span class="onoffswitch-switch"></span>
</label>
</div>
<br>
<label for="client-name">Client Name</label>
<input id="client-name" type="text" placeholder="Enter the client's name">
<br>
<label for="negotiator-name">Negotiator Name</label>
<input id="negotiator-name" type="text" placeholder="Enter the negotiator's name">
<br>
<label for="taker-crew-name">Crew Name</label>
<input id="taker-crew-name" type="text" placeholder="Enter the taker's crew's name">
<br>
<label for="takers">Numer of takers in the crew</label>
<input id="takers" type="text" pattern="[0-9]+" size="1">
<br>
</div>
</div>
<br>
<label for="is-first-negotiation">First Negotiation?</label>
<input id="is-first-negotiation" type="checkbox">
<br>
<label for="client-name">Client Name</label>
<input id="client-name" type="text" placeholder="Enter the client's name">
<br>
<label for="negotiator-name">Negotiator Name</label>
<input id="negotiator-name" type="text" placeholder="Enter the negotiator's name">
<br>
<label for="taker-crew-name">Crew Name</label>
<input id="taker-crew-name" type="text" placeholder="Enter the taker's crew's name">
<br>
<label for="takers">Numer of takers in the crew</label>
<input id="takers" type="text" pattern="[0-9]+" size="1">
<br>
<label for="first-impression-black">First Impression (Black die value)</label>
<label for="first-impression-black">First Impression (Black die value + Taker's leadership)</label>
<input id="first-impression-black" type="text" pattern="[0-9]+" size="1">
<br>
<label for="first-impression-state">Level of success on the first impression</label>
@ -44,6 +77,13 @@
</select>
</form>
</div>
{% else %}
<div class="negotiation-summary">
<h4 class="phase-display-name">
{{ phase.display_name }}
</h4>
<p class="phase-description-taker">{{ phase.description_taker }}</p>
</div>
{% endif %}
<div class="participants">