lila/ui/lobby/src/setup.ts

447 lines
14 KiB
TypeScript

import { FormStore, toFormLines, makeStore } from './form';
import modal from 'common/modal';
import debounce from 'common/debounce';
import * as xhr from 'common/xhr';
import LobbyController from './ctrl';
export default class Setup {
stores: {
hook: FormStore;
friend: FormStore;
ai: FormStore;
};
constructor(readonly makeStorage: (name: string) => LichessStorage, readonly root: LobbyController) {
this.stores = {
hook: makeStore(makeStorage('lobby.setup.hook')),
friend: makeStore(makeStorage('lobby.setup.friend')),
ai: makeStore(makeStorage('lobby.setup.ai')),
};
}
private save = (form: HTMLFormElement) => this.stores[form.getAttribute('data-type')!].set(toFormLines(form));
private sliderTimes = [
0,
1 / 4,
1 / 2,
3 / 4,
1,
3 / 2,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
25,
30,
35,
40,
45,
60,
75,
90,
105,
120,
135,
150,
165,
180,
];
private sliderTime = (v: number) => (v < this.sliderTimes.length ? this.sliderTimes[v] : 180);
private sliderIncrement = (v: number) => {
if (v <= 20) return v;
switch (v) {
case 21:
return 25;
case 22:
return 30;
case 23:
return 35;
case 24:
return 40;
case 25:
return 45;
case 26:
return 60;
case 27:
return 90;
case 28:
return 120;
case 29:
return 150;
default:
return 180;
}
};
private sliderDays = (v: number) => {
if (v <= 3) return v;
switch (v) {
case 4:
return 5;
case 5:
return 7;
case 6:
return 10;
default:
return 14;
}
};
private sliderInitVal = (v: number, f: (x: number) => number, max: number) => {
for (let i = 0; i < max; i++) {
if (f(i) === v) return i;
}
return undefined;
};
private hookToPoolMember = (color: string, form: HTMLFormElement) => {
const data = Array.from(new FormData(form).entries());
const hash: any = {};
for (const i in data) hash[data[i][0]] = data[i][1];
const valid = color == 'random' && hash.variant == 1 && hash.mode == 1 && hash.timeMode == 1,
id = parseFloat(hash.time) + '+' + parseInt(hash.increment);
return valid && this.root.pools.find(p => p.id === id)
? {
id,
range: hash.ratingRange,
}
: undefined;
};
prepareForm = ($modal: Cash) => {
const self = this,
$form = $modal.find('form'),
$timeModeSelect = $form.find('#sf_timeMode'),
$modeChoicesWrap = $form.find('.mode_choice'),
$modeChoices = $modeChoicesWrap.find('input'),
$casual = $modeChoices.eq(0),
$rated = $modeChoices.eq(1),
$variantSelect = $form.find('#sf_variant'),
$fenPosition = $form.find('.fen_position'),
$fenInput = $fenPosition.find('input'),
forceFormPosition = !!$fenInput.val(),
$timeInput = $form.find('.time_choice [name=time]'),
$incrementInput = $form.find('.increment_choice [name=increment]'),
$daysInput = $form.find('.days_choice [name=days]'),
typ = $form.data('type'),
$ratings = $modal.find('.ratings > div'),
randomColorVariants = $form.data('random-color-variants').split(','),
$submits = $form.find('.color-submits__button'),
toggleButtons = () => {
const variantId = $variantSelect.val(),
timeMode = $timeModeSelect.val(),
rated = $rated.prop('checked'),
limit = parseFloat($timeInput.val() as string),
inc = parseFloat($incrementInput.val() as string),
// no rated variants with less than 30s on the clock
cantBeRated = variantId != '1' && (timeMode != '1' || (limit < 0.5 && inc == 0) || (limit == 0 && inc < 2));
if (cantBeRated && rated) {
$casual.trigger('click');
return toggleButtons();
}
$rated.prop('disabled', !!cantBeRated).siblings('label').toggleClass('disabled', cantBeRated);
const timeOk = timeMode != '1' || limit > 0 || inc > 0,
ratedOk = typ != 'hook' || !rated || timeMode != '0',
aiOk = typ != 'ai' || variantId != '3' || limit >= 1;
if (timeOk && ratedOk && aiOk) {
$submits.toggleClass('nope', false);
$submits.filter(':not(.random)').toggle(!rated || !randomColorVariants.includes(variantId));
} else $submits.toggleClass('nope', true);
},
save = function () {
self.save($form[0] as HTMLFormElement);
};
const c = this.stores[typ].get();
if (c) {
Object.keys(c).forEach(k => {
$form.find(`[name="${k}"]`).each(function (this: HTMLInputElement) {
if (this.type == 'checkbox') this.checked = true;
else if (this.type == 'radio') this.checked = this.value == c[k];
else if (k != 'fen' || !this.value) this.value = c[k];
});
});
}
const showRating = () => {
const timeMode = $timeModeSelect.val();
let key = 'correspondence';
switch ($variantSelect.val()) {
case '1':
case '3':
if (timeMode == '1') {
const time = parseFloat($timeInput.val() as string) * 60 + parseFloat($incrementInput.val() as string) * 40;
if (time < 30) key = 'ultraBullet';
else if (time < 180) key = 'bullet';
else if (time < 480) key = 'blitz';
else if (time < 1500) key = 'rapid';
else key = 'classical';
}
break;
case '10':
key = 'crazyhouse';
break;
case '2':
key = 'chess960';
break;
case '4':
key = 'kingOfTheHill';
break;
case '5':
key = 'threeCheck';
break;
case '6':
key = 'antichess';
break;
case '7':
key = 'atomic';
break;
case '8':
key = 'horde';
break;
case '9':
key = 'racingKings';
break;
}
const $selected = $ratings
.hide()
.filter('.' + key)
.show();
$modal.find('.ratings input').val($selected.find('strong').text());
save();
};
if (typ == 'hook') {
if ($form.data('anon')) {
$timeModeSelect
.val('1')
.children('.timeMode_2, .timeMode_0')
.prop('disabled', true)
.attr('title', this.root.trans('youNeedAnAccountToDoThat'));
}
const ajaxSubmit = (color: string) => {
const form = $form[0] as HTMLFormElement;
const rating = parseInt($modal.find('.ratings input').val() as string) || 1500;
if (form.ratingRange)
form.ratingRange.value = [
rating + parseInt(form.ratingRange_range_min.value),
rating + parseInt(form.ratingRange_range_max.value),
].join('-');
save();
const poolMember = this.hookToPoolMember(color, form);
modal.close();
if (poolMember) {
this.root.enterPool(poolMember);
this.root.redraw();
} else {
this.root.setTab($timeModeSelect.val() === '1' ? 'real_time' : 'seeks');
xhr.text($form.attr('action')!.replace(/sri-placeholder/, lichess.sri), {
method: 'post',
body: (() => {
const data = new FormData($form[0] as HTMLFormElement);
data.append('color', color);
return data;
})(),
});
}
return false;
};
$submits
.on('click', function (this: HTMLElement) {
return ajaxSubmit($(this).val() as string);
})
.prop('disabled', false);
$form.on('submit', () => ajaxSubmit('random'));
} else
$form.one('submit', () => {
$submits.hide();
$form.find('.color-submits').append(lichess.spinnerHtml);
});
if (this.root.opts.blindMode) {
$variantSelect[0]!.focus();
$timeInput.add($incrementInput).on('change', () => {
toggleButtons();
showRating();
});
} else {
$timeInput.add($incrementInput).each(function (this: HTMLInputElement) {
const $input = $(this),
$value = $input.siblings('span'),
$range = $input.siblings('.range'),
isTimeSlider = $input.parent().hasClass('time_choice'),
showTime = (v: number) => {
if (v == 1 / 4) return '¼';
if (v == 1 / 2) return '½';
if (v == 3 / 4) return '¾';
return '' + v;
},
valueToTime = (v: number) => (isTimeSlider ? self.sliderTime : self.sliderIncrement)(v),
show = (time: number) => $value.text(isTimeSlider ? showTime(time) : '' + time);
show(parseFloat($input.val() as string));
$range.attr({
min: '0',
max: '' + (isTimeSlider ? 38 : 30),
value:
'' +
self.sliderInitVal(
parseFloat($input.val() as string),
isTimeSlider ? self.sliderTime : self.sliderIncrement,
100
),
});
$range.on('input', () => {
const time = valueToTime(parseInt($range.val() as string));
show(time);
$input.val('' + time);
showRating();
toggleButtons();
});
});
$daysInput.each(function (this: HTMLInputElement) {
var $input = $(this),
$value = $input.siblings('span'),
$range = $input.siblings('.range');
$value.text($input.val() as string);
$range.attr({
min: '1',
max: '7',
value: '' + self.sliderInitVal(parseInt($input.val() as string), self.sliderDays, 20),
});
$range.on('input', () => {
const days = self.sliderDays(parseInt($range.val() as string));
$value.text('' + days);
$input.val('' + days);
save();
});
});
$form.find('.rating-range').each(function (this: HTMLDivElement) {
const $this = $(this),
$minInput = $this.find('.rating-range__min'),
$maxInput = $this.find('.rating-range__max'),
minStorage = self.makeStorage('lobby.ratingRange.min'),
maxStorage = self.makeStorage('lobby.ratingRange.max'),
update = (e?: Event) => {
const min = $minInput.val() as string,
max = $maxInput.val() as string;
minStorage.set(min);
maxStorage.set(max);
$this.find('.rating-min').text(`-${min.replace('-', '')}`);
$this.find('.rating-max').text(`+${max}`);
if (e) save();
};
$minInput
.attr({
min: '-500',
max: '0',
step: '50',
value: minStorage.get() || '-500',
})
.on('input', update);
$maxInput
.attr({
min: '0',
max: '500',
step: '50',
value: maxStorage.get() || '500',
})
.on('input', update);
update();
});
}
$timeModeSelect
.on('change', function (this: HTMLElement) {
var timeMode = $(this).val();
$form.find('.time_choice, .increment_choice').toggle(timeMode == '1');
$form.find('.days_choice').toggle(timeMode == '2');
toggleButtons();
showRating();
})
.trigger('change');
var validateFen = debounce(() => {
$fenInput.removeClass('success failure');
var fen = $fenInput.val() as string;
if (fen) {
var [path, params] = $fenInput.parent().data('validate-url').split('?'); // Separate "strict=1" for AI match
xhr.text(xhr.url(path, { fen }) + (params ? `&${params}` : '')).then(
data => {
$fenInput.addClass('success');
$fenPosition.find('.preview').html(data);
$fenPosition.find('a.board_editor').each(function (this: HTMLAnchorElement) {
this.href = this.href.replace(/editor\/.+$/, 'editor/' + fen);
});
$submits.removeClass('nope');
lichess.contentLoaded();
},
_ => {
$fenInput.addClass('failure');
$fenPosition.find('.preview').html('');
$submits.addClass('nope');
}
);
}
}, 200);
$fenInput.on('keyup', validateFen);
if (forceFormPosition) $variantSelect.val('' + 3);
$variantSelect
.on('change', function (this: HTMLElement) {
var isFen = $(this).val() == '3';
$fenPosition.toggle(isFen);
$modeChoicesWrap.toggle(!isFen);
if (isFen) {
$casual.trigger('click');
requestAnimationFrame(() => document.body.dispatchEvent(new Event('chessground.resize')));
}
showRating();
toggleButtons();
})
.trigger('change');
$modeChoices.on('change', save);
$form.find('div.level').each(function (this: HTMLElement) {
var $infos = $(this).find('.ai_info > div');
$(this)
.find('label')
.on('mouseenter', function (this: HTMLElement) {
$infos
.hide()
.filter('.' + $(this).attr('for'))
.show();
});
$(this)
.find('#config_level')
.on('mouseleave', function (this: HTMLElement) {
var level = $(this).find('input:checked').val();
$infos
.hide()
.filter('.sf_level_' + level)
.show();
})
.trigger('mouseout');
$(this).find('input').on('change', save);
});
};
}