asterism-games 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -0
- data/README.md +67 -9
- data/VERSION +1 -1
- data/lib/asterism_games/commentate/assets/SOUNDS_LICENSE.txt +63 -0
- data/lib/asterism_games/commentate/assets/commentate.css +9 -4
- data/lib/asterism_games/commentate/assets/commentate.js +298 -7
- data/lib/asterism_games/commentate/assets/index.html +5 -3
- data/lib/asterism_games/commentate/assets/sounds/after_the_rain.mp3 +0 -0
- data/lib/asterism_games/commentate/assets/sounds/chi.wav +0 -0
- data/lib/asterism_games/commentate/assets/sounds/discard.wav +0 -0
- data/lib/asterism_games/commentate/assets/sounds/discard_other.wav +0 -0
- data/lib/asterism_games/commentate/assets/sounds/draw.wav +0 -0
- data/lib/asterism_games/commentate/assets/sounds/encounter.mp3 +0 -0
- data/lib/asterism_games/commentate/assets/sounds/fanfare1.wav +0 -0
- data/lib/asterism_games/commentate/assets/sounds/fanfare2.wav +0 -0
- data/lib/asterism_games/commentate/assets/sounds/fanfare3.wav +0 -0
- data/lib/asterism_games/commentate/assets/sounds/fanfare4.wav +0 -0
- data/lib/asterism_games/commentate/assets/sounds/kan.wav +0 -0
- data/lib/asterism_games/commentate/assets/sounds/kyuushu.wav +0 -0
- data/lib/asterism_games/commentate/assets/sounds/points.wav +0 -0
- data/lib/asterism_games/commentate/assets/sounds/pon.wav +0 -0
- data/lib/asterism_games/commentate/assets/sounds/riichi.wav +0 -0
- data/lib/asterism_games/commentate/assets/sounds/ron.wav +0 -0
- data/lib/asterism_games/commentate/assets/sounds/round.wav +0 -0
- data/lib/asterism_games/commentate/assets/sounds/ryuukyoku.wav +0 -0
- data/lib/asterism_games/commentate/assets/sounds/sounds.json +335 -0
- data/lib/asterism_games/commentate/assets/sounds/spring_thing.mp3 +0 -0
- data/lib/asterism_games/commentate/assets/sounds/tsumo.wav +0 -0
- data/lib/asterism_games/commentate/assets/sounds/turn.wav +0 -0
- data/lib/asterism_games/commentate/assets/sounds/yaku.wav +0 -0
- data/lib/asterism_games/commentate/mahjong.rb +11 -4
- data/lib/asterism_games/commentate/mahjong_sounds.rb +118 -0
- data/lib/asterism_games/commentate/server.rb +14 -2
- data/lib/asterism_games/commentate.rb +8 -2
- data/lib/asterism_games/play/mahjong.rb +3 -1
- metadata +29 -8
|
@@ -8,8 +8,14 @@
|
|
|
8
8
|
// the browser's speech synthesis.
|
|
9
9
|
// The AI's one-liners (l.ai) come after the fixed lines, marked "AI" in
|
|
10
10
|
// the captions, and are the first to go when the voice falls behind.
|
|
11
|
-
//
|
|
12
|
-
//
|
|
11
|
+
// The sound of Family mruby's mahjong app (README "The sound"): its BGM
|
|
12
|
+
// for the scene ("state"'s bgm) and its effects for each step ("state"'s
|
|
13
|
+
// sounds), files made with the app's own APU (/sounds/, sounds.json),
|
|
14
|
+
// played with Web Audio beside the voice; the BGM goes down while the
|
|
15
|
+
// voice speaks.
|
|
16
|
+
// Keys: Space pause, M voice, B BGM (off, low, mid, high), E effects,
|
|
17
|
+
// H hide the hands. URL parameters: table=<id>, hands=0 (hidden), voice=0,
|
|
18
|
+
// obs=1 (no bar, a transparent background).
|
|
13
19
|
"use strict";
|
|
14
20
|
|
|
15
21
|
(function () {
|
|
@@ -29,7 +35,7 @@
|
|
|
29
35
|
voiceOn: params.get("voice") !== "0",
|
|
30
36
|
unlocked: false, // the page was clicked: the browser lets it speak
|
|
31
37
|
paused: false,
|
|
32
|
-
held: { view: null, lines: [] },
|
|
38
|
+
held: { view: null, lines: [], bgm: null },
|
|
33
39
|
connected: false,
|
|
34
40
|
sheets: null,
|
|
35
41
|
};
|
|
@@ -399,7 +405,7 @@
|
|
|
399
405
|
const e = { t: Date.now(), kind: kind, id: item ? item.line.id : null, prio: item ? item.prio : null, src: src };
|
|
400
406
|
played.push(e);
|
|
401
407
|
if (played.length > 500) played.shift();
|
|
402
|
-
if (window.console) console.log("voice", kind, e.id, src);
|
|
408
|
+
if (window.console) console.log("voice", kind, e.id, src, e.t);
|
|
403
409
|
}
|
|
404
410
|
|
|
405
411
|
// Queues the line's voice; false when nothing is said (the caption then
|
|
@@ -468,16 +474,20 @@
|
|
|
468
474
|
if (!item) return;
|
|
469
475
|
speech.speaking = true;
|
|
470
476
|
speech.current = item;
|
|
477
|
+
sound.duck();
|
|
471
478
|
showLine(item.line);
|
|
472
479
|
sayItem(item).then(() => {
|
|
473
480
|
if (speech.current !== item) return; // cut off (M, H)
|
|
474
481
|
speech.current = null;
|
|
475
482
|
speech.speaking = false;
|
|
483
|
+
sound.duck();
|
|
476
484
|
setTimeout(next, 120);
|
|
477
485
|
});
|
|
478
486
|
}
|
|
479
487
|
|
|
480
488
|
async function sayItem(item) {
|
|
489
|
+
await sound.afterJingle();
|
|
490
|
+
if (speech.current !== item) return;
|
|
481
491
|
if (item.clips || item.blob) {
|
|
482
492
|
for (const c of item.clips || []) {
|
|
483
493
|
if (speech.current !== item) return;
|
|
@@ -536,6 +546,7 @@
|
|
|
536
546
|
drop(() => true, "cut");
|
|
537
547
|
speech.current = null;
|
|
538
548
|
speech.speaking = false;
|
|
549
|
+
sound.duck();
|
|
539
550
|
}
|
|
540
551
|
|
|
541
552
|
function showVoice() {
|
|
@@ -559,17 +570,20 @@
|
|
|
559
570
|
if (st.unlocked) return;
|
|
560
571
|
st.unlocked = true;
|
|
561
572
|
$("hint").hidden = true;
|
|
573
|
+
sound.start();
|
|
562
574
|
// A first sound inside the gesture opens the way on some browsers.
|
|
563
575
|
const v = vv();
|
|
564
576
|
if (st.voiceOn && canSpeak()) {
|
|
565
577
|
const item = { line: { id: 0 }, prio: 1, at: Date.now() };
|
|
566
578
|
speech.speaking = true;
|
|
567
579
|
speech.current = item;
|
|
580
|
+
sound.duck();
|
|
568
581
|
const p = v && v.start ? (log("clip", item, v.start), playUrl(v.start)) : speak(item, "実況を始めます");
|
|
569
582
|
p.then(() => {
|
|
570
583
|
if (speech.current !== item) return;
|
|
571
584
|
speech.current = null;
|
|
572
585
|
speech.speaking = false;
|
|
586
|
+
sound.duck();
|
|
573
587
|
next();
|
|
574
588
|
});
|
|
575
589
|
}
|
|
@@ -596,6 +610,270 @@
|
|
|
596
610
|
}
|
|
597
611
|
}
|
|
598
612
|
|
|
613
|
+
// ------------------------------------------------------------ the app's sound
|
|
614
|
+
|
|
615
|
+
// The BGM and the effects of Family mruby's mahjong app, as files made
|
|
616
|
+
// with its APU (sounds.json lists them: the effects' priorities and
|
|
617
|
+
// channels, the tunes' loops, the app's BGM levels and scenes). Web Audio,
|
|
618
|
+
// after the first click (the browser's autoplay rule): the BGM is the
|
|
619
|
+
// scene's tune from its start, its loop repeated without a seam; the
|
|
620
|
+
// levels are the app's (off, low, mid, high: low by default, as the
|
|
621
|
+
// app's), and while the voice speaks the BGM is at DUCK of its level
|
|
622
|
+
// (the effects stay as they are). An effect plays at once, beside the
|
|
623
|
+
// voice (not in its queue), and claims the APU channels it uses as the
|
|
624
|
+
// app's do: one of a higher priority still sounding on them keeps them
|
|
625
|
+
// (the new one is left out when it gets none), otherwise the older
|
|
626
|
+
// sounds on them stop. A voice that would start on top of a jingle
|
|
627
|
+
// waits for most of it (at most JINGLE_WAIT_MS). B and E switch, and the
|
|
628
|
+
// page remembers both (localStorage).
|
|
629
|
+
const sound = {
|
|
630
|
+
ctx: null,
|
|
631
|
+
manifest: null,
|
|
632
|
+
buffers: {}, // file => AudioBuffer (or a Promise of one)
|
|
633
|
+
bgmLevel: null,
|
|
634
|
+
seOn: true,
|
|
635
|
+
scene: "lobby",
|
|
636
|
+
bgm: null, // {key, src, gain} playing
|
|
637
|
+
bgmGain: null,
|
|
638
|
+
seGain: null,
|
|
639
|
+
ducked: false,
|
|
640
|
+
duckTimer: null,
|
|
641
|
+
holders: [null, null, null, null], // per APU channel: {pri, until, src}
|
|
642
|
+
jingleUntil: 0,
|
|
643
|
+
paused: false,
|
|
644
|
+
log: [],
|
|
645
|
+
DUCK: 0.5,
|
|
646
|
+
JINGLE_WAIT_MS: 600,
|
|
647
|
+
STORE: "asterism-games.commentate.",
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
sound.note = function (kind, name) {
|
|
651
|
+
const e = { t: Date.now(), kind: kind, name: name };
|
|
652
|
+
sound.log.push(e);
|
|
653
|
+
if (sound.log.length > 500) sound.log.shift();
|
|
654
|
+
if (window.console) console.log("sound", kind, name, e.t);
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
function stored(key) {
|
|
658
|
+
try { return window.localStorage.getItem(sound.STORE + key); } catch (e) { return null; }
|
|
659
|
+
}
|
|
660
|
+
function store(key, value) {
|
|
661
|
+
try { window.localStorage.setItem(sound.STORE + key, String(value)); } catch (e) { /* not kept */ }
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
sound.levels = function () { return (sound.manifest && sound.manifest.bgm_levels) || [0, 0.3, 0.55, 1]; };
|
|
665
|
+
sound.names = function () { return (sound.manifest && sound.manifest.bgm_names) || ["切", "小", "中", "大"]; };
|
|
666
|
+
|
|
667
|
+
sound.load = function () {
|
|
668
|
+
const lv = parseInt(stored("bgm"), 10);
|
|
669
|
+
sound.bgmLevel = lv >= 0 && lv <= 3 ? lv : null;
|
|
670
|
+
sound.seOn = stored("se") !== "0";
|
|
671
|
+
fetch("/sounds/sounds.json").then((r) => (r.ok ? r.json() : Promise.reject(new Error("HTTP " + r.status))))
|
|
672
|
+
.then((m) => {
|
|
673
|
+
sound.manifest = m;
|
|
674
|
+
if (sound.bgmLevel === null || sound.bgmLevel >= m.bgm_levels.length) sound.bgmLevel = m.bgm_default;
|
|
675
|
+
sound.show();
|
|
676
|
+
if (sound.ctx) { sound.preload(); sound.applyBgm(); }
|
|
677
|
+
})
|
|
678
|
+
.catch(() => { sound.manifest = null; sound.show(); });
|
|
679
|
+
sound.show();
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
// The first click: the context, the two volumes, the effects' files.
|
|
683
|
+
sound.start = function () {
|
|
684
|
+
if (sound.ctx) return;
|
|
685
|
+
const AC = window.AudioContext || window.webkitAudioContext;
|
|
686
|
+
if (!AC) { sound.show(); return; }
|
|
687
|
+
// 24 kHz is plenty for the APU's sound (15.72 kHz) and halves what the
|
|
688
|
+
// decoded tunes take; a browser that will not have it gets its own rate.
|
|
689
|
+
try { sound.ctx = new AC({ sampleRate: 24000 }); } catch (e) { sound.ctx = new AC(); }
|
|
690
|
+
sound.bgmGain = sound.ctx.createGain();
|
|
691
|
+
sound.bgmGain.gain.value = sound.bgmVolume();
|
|
692
|
+
sound.bgmGain.connect(sound.ctx.destination);
|
|
693
|
+
sound.seGain = sound.ctx.createGain();
|
|
694
|
+
sound.seGain.connect(sound.ctx.destination);
|
|
695
|
+
if (sound.ctx.state === "suspended") sound.ctx.resume().catch(() => {});
|
|
696
|
+
if (sound.manifest) { sound.preload(); sound.applyBgm(); }
|
|
697
|
+
sound.show();
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
sound.buffer = function (file) {
|
|
701
|
+
const b = sound.buffers[file];
|
|
702
|
+
if (b) return b instanceof Promise ? b : Promise.resolve(b);
|
|
703
|
+
const p = fetch("/sounds/" + file)
|
|
704
|
+
.then((r) => (r.ok ? r.arrayBuffer() : Promise.reject(new Error("HTTP " + r.status))))
|
|
705
|
+
.then((data) => new Promise((resolve, reject) => sound.ctx.decodeAudioData(data, resolve, reject)))
|
|
706
|
+
.then((buf) => { sound.buffers[file] = buf; return buf; })
|
|
707
|
+
.catch((e) => { delete sound.buffers[file]; sound.note("failed", file); throw e; });
|
|
708
|
+
sound.buffers[file] = p;
|
|
709
|
+
return p;
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
sound.preload = function () {
|
|
713
|
+
Object.keys(sound.manifest.effects).forEach((n) => sound.buffer(sound.manifest.effects[n].file).catch(() => {}));
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
sound.bgmVolume = function () {
|
|
717
|
+
const v = sound.levels()[sound.bgmLevel || 0] || 0;
|
|
718
|
+
return sound.ducked ? v * sound.DUCK : v;
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
sound.setScene = function (scene) {
|
|
722
|
+
if (scene === sound.scene) return;
|
|
723
|
+
sound.scene = scene;
|
|
724
|
+
sound.applyBgm();
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
// The tune of the scene, or none (off, the end of a game, paused).
|
|
728
|
+
sound.applyBgm = function () {
|
|
729
|
+
const m = sound.manifest;
|
|
730
|
+
if (!sound.ctx || !m) return;
|
|
731
|
+
let key = m.scenes[sound.scene] || null;
|
|
732
|
+
if (!key && sound.scene === "tense") key = m.scenes.play || null;
|
|
733
|
+
if (!sound.bgmLevel || sound.paused) key = null;
|
|
734
|
+
const cur = sound.bgm;
|
|
735
|
+
if (cur && cur.key === key) return;
|
|
736
|
+
if (cur) {
|
|
737
|
+
const t = sound.ctx.currentTime;
|
|
738
|
+
cur.gain.gain.setTargetAtTime(0, t, 0.03);
|
|
739
|
+
cur.src.stop(t + 0.2);
|
|
740
|
+
sound.bgm = null;
|
|
741
|
+
}
|
|
742
|
+
if (!key) return;
|
|
743
|
+
const tune = m.tunes[key];
|
|
744
|
+
const want = { key: key, src: null, gain: null };
|
|
745
|
+
sound.bgm = want;
|
|
746
|
+
sound.buffer(tune.file).then((buf) => {
|
|
747
|
+
if (sound.bgm !== want) return;
|
|
748
|
+
const src = sound.ctx.createBufferSource();
|
|
749
|
+
src.buffer = buf;
|
|
750
|
+
src.loop = true;
|
|
751
|
+
src.loopStart = tune.loop_start;
|
|
752
|
+
src.loopEnd = tune.loop_start + tune.loop_s;
|
|
753
|
+
const g = sound.ctx.createGain();
|
|
754
|
+
src.connect(g);
|
|
755
|
+
g.connect(sound.bgmGain);
|
|
756
|
+
src.start();
|
|
757
|
+
want.src = src;
|
|
758
|
+
want.gain = g;
|
|
759
|
+
sound.note("bgm", key);
|
|
760
|
+
}).catch(() => { if (sound.bgm === want) sound.bgm = null; });
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
// An effect of the app (name: sounds.json's effects), delay ms from now.
|
|
764
|
+
sound.effect = function (name, delay) {
|
|
765
|
+
if (!sound.ctx || !sound.seOn || sound.paused || !sound.manifest) return;
|
|
766
|
+
const e = sound.manifest.effects[name];
|
|
767
|
+
if (!e) return;
|
|
768
|
+
const go = () => {
|
|
769
|
+
if (!sound.seOn || sound.paused) return;
|
|
770
|
+
sound.buffer(e.file).then((buf) => sound.play(name, e, buf)).catch(() => {});
|
|
771
|
+
};
|
|
772
|
+
if (delay > 0) setTimeout(go, delay); else go();
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
sound.play = function (name, e, buf) {
|
|
776
|
+
const now = Date.now();
|
|
777
|
+
const free = e.channels.filter((ch) => {
|
|
778
|
+
const h = sound.holders[ch];
|
|
779
|
+
return !h || h.until <= now || e.priority >= h.pri;
|
|
780
|
+
});
|
|
781
|
+
if (!free.length) { sound.note("left out", name); return; }
|
|
782
|
+
const src = sound.ctx.createBufferSource();
|
|
783
|
+
src.buffer = buf;
|
|
784
|
+
src.connect(sound.seGain);
|
|
785
|
+
const until = now + buf.duration * 1000;
|
|
786
|
+
free.forEach((ch) => {
|
|
787
|
+
const h = sound.holders[ch];
|
|
788
|
+
if (h && h.until > now && h.src !== src) {
|
|
789
|
+
try { h.src.stop(); } catch (x) { /* ended */ }
|
|
790
|
+
}
|
|
791
|
+
sound.holders[ch] = { pri: e.priority, until: until, src: src };
|
|
792
|
+
});
|
|
793
|
+
src.start();
|
|
794
|
+
if (e.priority >= 3) sound.jingleUntil = Math.max(sound.jingleUntil, until);
|
|
795
|
+
sound.note("sfx", name);
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
// Resolves when a voice may start: after most of a sounding jingle.
|
|
799
|
+
sound.afterJingle = function () {
|
|
800
|
+
const wait = Math.min(sound.JINGLE_WAIT_MS, sound.jingleUntil - Date.now() - 120);
|
|
801
|
+
return wait > 0 ? new Promise((resolve) => setTimeout(resolve, wait)) : Promise.resolve();
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
// The BGM down while the voice speaks; up again a moment after it stops
|
|
805
|
+
// (not between two lines).
|
|
806
|
+
sound.duck = function () {
|
|
807
|
+
const on = speech.speaking;
|
|
808
|
+
if (on) {
|
|
809
|
+
if (sound.duckTimer) { clearTimeout(sound.duckTimer); sound.duckTimer = null; }
|
|
810
|
+
if (!sound.ducked) { sound.ducked = true; sound.volume(0.05); sound.note("duck", "down"); }
|
|
811
|
+
} else if (sound.ducked && !sound.duckTimer) {
|
|
812
|
+
sound.duckTimer = setTimeout(() => {
|
|
813
|
+
sound.duckTimer = null;
|
|
814
|
+
if (speech.speaking) return;
|
|
815
|
+
sound.ducked = false;
|
|
816
|
+
sound.volume(0.25);
|
|
817
|
+
sound.note("duck", "up");
|
|
818
|
+
}, 600);
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
sound.volume = function (tc) {
|
|
823
|
+
if (!sound.bgmGain) return;
|
|
824
|
+
const t = sound.ctx.currentTime;
|
|
825
|
+
sound.bgmGain.gain.cancelScheduledValues(t);
|
|
826
|
+
sound.bgmGain.gain.setTargetAtTime(sound.bgmVolume(), t, tc || 0.05);
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
sound.cycleBgm = function () {
|
|
830
|
+
const n = sound.levels().length;
|
|
831
|
+
sound.bgmLevel = ((sound.bgmLevel || 0) + 1) % n;
|
|
832
|
+
store("bgm", sound.bgmLevel);
|
|
833
|
+
sound.volume(0.05);
|
|
834
|
+
sound.applyBgm();
|
|
835
|
+
sound.show();
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
sound.toggleSe = function () {
|
|
839
|
+
sound.seOn = !sound.seOn;
|
|
840
|
+
store("se", sound.seOn ? 1 : 0);
|
|
841
|
+
if (!sound.seOn) {
|
|
842
|
+
sound.holders.forEach((h) => { if (h) { try { h.src.stop(); } catch (x) { /* ended */ } } });
|
|
843
|
+
sound.holders = [null, null, null, null];
|
|
844
|
+
}
|
|
845
|
+
sound.show();
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
sound.pause = function (p) {
|
|
849
|
+
sound.paused = p;
|
|
850
|
+
if (!sound.ctx) return;
|
|
851
|
+
if (p) sound.ctx.suspend().catch(() => {});
|
|
852
|
+
else sound.ctx.resume().catch(() => {});
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
sound.show = function () {
|
|
856
|
+
const b = $("bgm");
|
|
857
|
+
const e = $("se");
|
|
858
|
+
const credit = $("bgmcredit");
|
|
859
|
+
if (!sound.manifest) {
|
|
860
|
+
b.textContent = "BGM: なし";
|
|
861
|
+
b.className = "item";
|
|
862
|
+
e.textContent = "";
|
|
863
|
+
credit.hidden = true;
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
const lv = sound.bgmLevel || 0;
|
|
867
|
+
const wait = st.unlocked ? "" : " (クリック待ち)";
|
|
868
|
+
b.textContent = "BGM " + sound.names()[lv] + " [B]" + (lv ? wait : "");
|
|
869
|
+
b.className = "item btn" + (lv ? " on" : "");
|
|
870
|
+
e.textContent = "効果音 " + (sound.seOn ? "入" : "切") + " [E]";
|
|
871
|
+
e.className = "item btn" + (sound.seOn ? " on" : "");
|
|
872
|
+
credit.hidden = !lv;
|
|
873
|
+
};
|
|
874
|
+
$("bgm").addEventListener("click", () => sound.cycleBgm());
|
|
875
|
+
$("se").addEventListener("click", () => sound.toggleSe());
|
|
876
|
+
|
|
599
877
|
// ------------------------------------------------------------ the bar
|
|
600
878
|
|
|
601
879
|
function showBar() {
|
|
@@ -627,6 +905,7 @@
|
|
|
627
905
|
$("conn").className = "item warn";
|
|
628
906
|
showVoice();
|
|
629
907
|
showAI();
|
|
908
|
+
sound.show();
|
|
630
909
|
}
|
|
631
910
|
|
|
632
911
|
$("tables").addEventListener("change", (e) => {
|
|
@@ -644,6 +923,7 @@
|
|
|
644
923
|
|
|
645
924
|
document.addEventListener("keydown", (e) => {
|
|
646
925
|
if (e.target && e.target.tagName === "SELECT") return;
|
|
926
|
+
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
|
647
927
|
if (e.key === " ") {
|
|
648
928
|
e.preventDefault();
|
|
649
929
|
setPaused(!st.paused);
|
|
@@ -652,6 +932,12 @@
|
|
|
652
932
|
if (!st.voiceOn) cut();
|
|
653
933
|
unlock();
|
|
654
934
|
showVoice();
|
|
935
|
+
} else if (e.key === "b" || e.key === "B") {
|
|
936
|
+
sound.cycleBgm();
|
|
937
|
+
unlock();
|
|
938
|
+
} else if (e.key === "e" || e.key === "E") {
|
|
939
|
+
sound.toggleSe();
|
|
940
|
+
unlock();
|
|
655
941
|
} else if (e.key === "h" || e.key === "H") {
|
|
656
942
|
st.hide = !st.hide;
|
|
657
943
|
cut();
|
|
@@ -670,10 +956,12 @@
|
|
|
670
956
|
st.paused = p;
|
|
671
957
|
if (synth) { if (p) synth.pause(); else synth.resume(); }
|
|
672
958
|
if (speech.audio) { if (p) speech.audio.pause(); else speech.audio.play().catch(() => {}); }
|
|
959
|
+
sound.pause(p);
|
|
673
960
|
if (!p) {
|
|
674
961
|
if (st.held.view) st.view = st.held.view;
|
|
962
|
+
if (st.held.bgm) sound.setScene(st.held.bgm);
|
|
675
963
|
const held = st.held.lines;
|
|
676
|
-
st.held = { view: null, lines: [] };
|
|
964
|
+
st.held = { view: null, lines: [], bgm: null };
|
|
677
965
|
const big = held.filter((l) => (l.prio || 0) >= 3).pop();
|
|
678
966
|
held.forEach((l) => { st.lines.push(l); if (l !== big) addCaption(l); });
|
|
679
967
|
if (big && !say(big)) addCaption(big);
|
|
@@ -707,8 +995,10 @@
|
|
|
707
995
|
const d = JSON.parse(e.data);
|
|
708
996
|
if (st.table && d.table !== st.table) return;
|
|
709
997
|
if (!st.table) st.table = d.table;
|
|
710
|
-
if (st.paused) { st.held.view = d.view; return; }
|
|
998
|
+
if (st.paused) { st.held.view = d.view; st.held.bgm = d.bgm; return; }
|
|
711
999
|
st.view = d.view;
|
|
1000
|
+
sound.setScene(d.bgm || "lobby");
|
|
1001
|
+
(d.sounds || []).forEach((x) => sound.effect(x[0], x[1]));
|
|
712
1002
|
draw();
|
|
713
1003
|
showBar();
|
|
714
1004
|
});
|
|
@@ -730,6 +1020,7 @@
|
|
|
730
1020
|
draw();
|
|
731
1021
|
showBar();
|
|
732
1022
|
connect();
|
|
1023
|
+
sound.load();
|
|
733
1024
|
// For a test or a tool driving the page.
|
|
734
|
-
window.commentate = { state: st, played: played, speech: speech };
|
|
1025
|
+
window.commentate = { state: st, played: played, speech: speech, sound: sound };
|
|
735
1026
|
})();
|
|
@@ -11,16 +11,18 @@
|
|
|
11
11
|
<span id="tname" class="item strong">卓を待っています</span>
|
|
12
12
|
<select id="tables" class="item" title="卓" hidden></select>
|
|
13
13
|
<span id="voice" class="item" title="M で切り替え">声: 切</span>
|
|
14
|
+
<span id="bgm" class="item btn" role="button" title="B で切り替え (切/小/中/大)">BGM 小 [B]</span>
|
|
15
|
+
<span id="se" class="item btn" role="button" title="E で切り替え">効果音 入 [E]</span>
|
|
14
16
|
<span id="ai" class="item"></span>
|
|
15
17
|
<span id="delay" class="item"></span>
|
|
16
18
|
<span id="mode" class="item"></span>
|
|
17
19
|
<span id="conn" class="item"></span>
|
|
18
20
|
</header>
|
|
19
|
-
<div id="hint" role="button" tabindex="0"
|
|
21
|
+
<div id="hint" role="button" tabindex="0">画面をクリックすると実況の声と音が出ます (ブラウザの自動再生の決まり)</div>
|
|
20
22
|
<main id="stage"><canvas id="table" aria-label="卓"></canvas></main>
|
|
21
23
|
<section id="captions" aria-live="polite"></section>
|
|
22
|
-
<footer id="keys">Space 一時停止 / M 声 / H 手牌を隠す</footer>
|
|
23
|
-
<div id="credit" hidden></div>
|
|
24
|
+
<footer id="keys">Space 一時停止 / M 声 / B BGM / E 効果音 / H 手牌を隠す</footer>
|
|
25
|
+
<div id="credits"><span id="credit" hidden></span><span id="bgmcredit" hidden>BGM: shiru8bit (CC BY 3.0)</span></div>
|
|
24
26
|
<script src="/commentate.js"></script>
|
|
25
27
|
</body>
|
|
26
28
|
</html>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|