UNPKG

skynovel

Version:
595 lines (511 loc) 20.9 kB
/* ***** BEGIN LICENSE BLOCK ***** Copyright (c) 2018-2020 Famibee (famibee.blog38.fc2.com) This software is released under the MIT License. http://opensource.org/licenses/mit-license.php ** ***** END LICENSE BLOCK ***** */ import {CmnLib, uint, IEvtMng, argChk_Boolean, argChk_Num} from './CmnLib'; import {HArg, IHTag, IVariable, IMain, IEvt2Fnc, IHEvt2Fnc} from './CmnInterface'; import {LayerMng} from './LayerMng'; import {ScriptIterator} from './ScriptIterator'; import {TxtLayer} from './TxtLayer'; import {EventListenerCtn} from './EventListenerCtn'; const Tween = require('@tweenjs/tween.js').default; import {interaction, DisplayObject, Application} from 'pixi.js'; import {SoundMng} from './SoundMng'; import {Config} from './Config'; import {SysBase} from './SysBase'; import Hammer = require('hammerjs'); export class EventMng implements IEvtMng { private readonly elc = new EventListenerCtn; private ham : any; private readonly hHamEv :{[name: string]: null | {(e: any): void}} = { // tap : null, tap2 : null, press : null, // 長押し //swipe : null, swipeleft : null, swiperight : null, swipeup : null, swipedown : null, }; constructor(private readonly cfg: Config, private readonly hTag: IHTag, private readonly appPixi: Application, private readonly main: IMain, private readonly layMng: LayerMng, private readonly val: IVariable, private readonly sndMng: SoundMng, private readonly scrItr: ScriptIterator, readonly sys: SysBase) { // イベント hTag.clear_event = o=> this.clear_event(o); // イベントを全消去 // enable_event // LayerMng.ts内で定義 //イベント有無の切替 hTag.event = o=> this.event(o); // イベントを予約 //hTag.gesture_event (形式変更) // ジェスチャイベントを予約 hTag.l = o=> this.l(o); // 行末クリック待ち hTag.p = o=> this.p(o); // 改ページクリック待ち hTag.s = ()=> {this.stdWait(()=> {}, false); return true;}; // 停止する // stdWait()したらreturn true; hTag.set_cancel_skip= ()=> this.set_cancel_skip(); // スキップ中断予約 hTag.wait = o=> this.wait(o); // ウェイトを入れる hTag.waitclick = ()=> {this.stdWait(()=> main.resume()); return true;}; // クリックを待つ // stdWait()したらreturn true; sndMng.setEvtMng(this); scrItr.setOtherObj(this, layMng); TxtLayer.setEvtMng(main, this); layMng.setEvtMng(this); sys.setFire((KEY, e)=> this.fire(KEY, e)); sys.addHook((type: string, o: object)=> this.procHook(type, o)); this.ham = new Hammer(appPixi.view, {recognizers: [ // [Hammer.Tap], [Hammer.Press], [Hammer.Swipe, {direction: Hammer.DIRECTION_ALL}], //[Hammer.Rotate], //[Hammer.Pinch, { enable: false }, ['rotate']], // http://hammerjs.github.io/api/ ]}); /*const manager = new Hammer.Manager(document.body, {recognizers: [ [Hammer.Tap, {pointers: 2}], ]});*/ // Add the recognizer to the manager. this.hHamEv.tap2 = null; for (const key in this.hHamEv) { const fnc = this.hHamEv[key] = (e: any)=> { val.defTmp('sn.eventArg.type', e.type); val.defTmp('sn.eventArg.pointers', e.pointers); this.fire(e.type, e); } this.ham.on(key, fnc); } /*manager.add(new Hammer.Tap({event: 'tap2', pointers: 2})); manager.on('tap2', e=> { val.defTmp('sn.eventArg', e.type); //console.log(e.type); // TODO: これどうなってる? 最前面にデバッグ情報表示ほしい。 //hTag['title']({text: 'DBG:'+ e.type}); this.evt2Fnc(e, e.type); });*/ appPixi.stage.interactive = true; if (CmnLib.isMobile) appPixi.stage.on('pointerdown', (e: any)=> this.fire('click', e)); else this.elc.add(appPixi.stage, 'pointerdown', e=> { switch (e.data.button) { case 0: this.fire('click', e); break; case 1: this.fire('middleclick', e); break; } }); this.elc.add(window, 'keydown', e=> this.ev_keydown(e)); this.elc.add(appPixi.view, 'contextmenu', e=> this.ev_contextmenu(e)); if ('WheelEvent' in window) { this.elc.add(appPixi.view, 'wheel', e=> this.ev_wheel(e), {passive: true}); this.resvFlameEvent4Wheel = (win: Window)=> win.addEventListener('wheel', e=> this.ev_wheel(e), {passive: true}); this.waitCustomEvent4Wheel = (elc: EventListenerCtn, fnc: ()=> void)=> elc.add(this.appPixi.view, 'wheel', (e: any)=> { //if (! e.isTrusted) return; if (e['isComposing']) return; // サポートしてない環境でもいける書き方 if (e.deltaY <= 0) return; e.stopPropagation(); fnc(); }); } // Gamepad APIの利用 - ウェブデベロッパーガイド | MDN https://developer.mozilla.org/ja/docs/Web/Guide/API/Gamepad // Gamepad の接続 this.elc.add(window, 'gamepadconnected', (e: any)=> { if (CmnLib.debugLog) console.log( '👺 Gamepad connected at index %d: %s. %d buttons, %d axes.', e['gamepad'].index, e['gamepad'].id, e['gamepad'].buttons.length, e['gamepad'].axes.length); const key = e.type; this.fire(key, e); }); // Gamepad の切断 this.elc.add(window, 'gamepaddisconnected', (e: any)=> { if (CmnLib.debugLog) console.log( '👺 Gamepad disconnected from index %d: %s', e['gamepad'].index, e['gamepad'].id); const key = e.type; this.fire(key, e); }); this.elc.add(window, 'keyup', (e: any)=> { if (e['isComposing']) return; // サポートしてない環境でもいける書き方 if (e.key in this.hDownKeys) this.hDownKeys[e.key] = 0; }); val.defTmp('const.sn.key.alternate', ()=> (this.hDownKeys['Alt'] > 0)); val.defTmp('const.sn.key.command', ()=> (this.hDownKeys['Meta'] > 0)); val.defTmp('const.sn.key.control', ()=> (this.hDownKeys['Control'] > 0)); val.defTmp('const.sn.key.end', ()=> (this.hDownKeys['End'] > 0)); val.defTmp('const.sn.key.escape', ()=> (this.hDownKeys['Escape'] > 0)); val.defTmp('const.sn.key.back', ()=> (this.hDownKeys['GoBack'] > 0)); } resvFlameEvent(win: Window) { win.addEventListener('keydown', e=> this.ev_keydown(e)); win.addEventListener('contextmenu', e=> this.ev_contextmenu(e)); this.resvFlameEvent4Wheel(win); } private resvFlameEvent4Wheel = (_win: Window)=> {}; private ev_keydown(e: any) { //if (! e.isTrusted) return; if (e['isComposing']) return; // サポートしてない環境でもいける書き方 if (e.key in this.hDownKeys) this.hDownKeys[e.key] = e.repeat ?2 :1; const key = (e.altKey ?(e.key === 'Alt' ?'' :'alt+') :'') + (e.ctrlKey ?(e.key === 'Control' ?'' :'ctrl+') :'') + (e.shiftKey ?(e.key === 'Shift' ?'' :'shift+') :'') + e.key; this.fire(key, e); } private ev_contextmenu(e: any) { //if (! e.isTrusted) return; const key = (e.altKey ?(e.key === 'Alt' ?'' :'alt+') :'') + (e.ctrlKey ?(e.key === 'Control' ?'' :'ctrl+') :'') + (e.shiftKey ?(e.key === 'Shift' ?'' :'shift+') :'') + 'rightclick'; this.fire(key, e); e.preventDefault(); // イベント未登録時、メニューが出てしまうので } private ev_wheel(e: any) { //if (! e.isTrusted) return; if (e['isComposing']) return; // サポートしてない環境でもいける書き方 if (this.wheeling) {this.extend_wheel = true; return;} this.wheeling = true; this.ev_wheel_waitstop(); // 今のところ縦回転ホイールのみ想定 const key = (e.altKey ?'alt+' :'') + (e.ctrlKey ?'ctrl+' :'') + (e.shiftKey ?'shift+' :'') + (e.deltaY > 0 ?'downwheel' :'upwheel'); this.fire(key, e); } private wheeling = false; private extend_wheel = false; private ev_wheel_waitstop() { setTimeout(()=> { if (this.extend_wheel) { this.extend_wheel = false; this.ev_wheel_waitstop(); return; } this.wheeling = false; }, 250); } destroy() { this.elc.clear(); for (const key in this.hHamEv) { this.ham.off(key); //this.ham.off(key, this.hHamEv[key]); } this.ham.destroy(); } private hLocalEvt2Fnc : IHEvt2Fnc = {}; private hGlobalEvt2Fnc : IHEvt2Fnc = {}; fire(KEY: string, e: Event) { if (this.isBreak) return; const key = KEY.toLowerCase(); //if (CmnLib.debugLog) console.log(`👺 <(key:\`${key}\` type:${e.type} e:%o)`, {...e}); const ke = this.hLocalEvt2Fnc[key] || this.hGlobalEvt2Fnc[key]; if (! ke) { if (key.slice(0, 5) === 'swipe') { // スマホ用疑似スワイプスクロール const esw: any = e; window.scrollBy(-(esw.deltaX ?? 0), -(esw.deltaY ?? 0)); } return; } if ((key.slice(-5) !== 'wheel') && ('preventDefault' in e)) e.preventDefault(); e.stopPropagation(); if (key.slice(0, 4) !== 'dom=' && this.layMng.clickTxtLay()) return; if (! this.isWait) return; this.isWait = false; ke(e); } private isWait = false; popLocalEvts(): IHEvt2Fnc { if (this.isWait) return {}; // [tsy]などのonComplete()から呼ばれた際の対応 const ret = this.hLocalEvt2Fnc; this.hLocalEvt2Fnc = {}; return ret; } pushLocalEvts(h: IHEvt2Fnc) {this.hLocalEvt2Fnc = h;} // stdWait()したらreturn true; stdWait(fnc: (e?: interaction.InteractionEvent)=> void, canskip = true) { this.goTxt(); if (canskip) { //hTag.event({key:'click', breakout: fnc}); //hTag.event({key:'middleclick', breakout: fnc}); // hTag.event()は内部で使わず、こうする const fncKey = ()=> fnc(); this.hLocalEvt2Fnc['click'] = fncKey; //this.hTag.event({key:'enter', breakout: fnc}); //hTag.event({key:'down', breakout: fnc}); // hTag.event()は内部で使わず、こうする this.hLocalEvt2Fnc['enter'] = fncKey; this.hLocalEvt2Fnc['arrowdown'] = fncKey; // hTag.event({key:'downwheel', breakout: fnc}); // hTag.event()は内部で使わず、こうする this.hLocalEvt2Fnc['wheel.y>0'] = fncKey; } else { delete this.hLocalEvt2Fnc['click']; delete this.hLocalEvt2Fnc['enter']; delete this.hLocalEvt2Fnc['arrowdown']; delete this.hLocalEvt2Fnc['wheel.y>0']; } // evtfncWait(); this.val.saveKidoku(); // これはそのままか this.fncCancelSkip(); this.isWait = true; } private procHook(type: string, _o: any): void { switch (type) { case 'continue': case 'disconnect': this.isBreak = false; break; default: this.isBreak = true; } } private isBreak = false; button(hArg: HArg, em: DisplayObject) { if (! hArg.fn && ! hArg.label) this.main.errScript('fnまたはlabelは必須です'); em.interactive = em.buttonMode = true; const key = (hArg.key ?? ' ').toLowerCase(); if (! hArg.fn) hArg.fn = this.scrItr.scriptFn; const glb = argChk_Boolean(hArg, 'global', false); if (glb) this.hGlobalEvt2Fnc[key] = ()=> this.main.resumeByJumpOrCall(hArg); else this.hLocalEvt2Fnc[key] = ()=> this.main.resumeByJumpOrCall(hArg); em.on('pointerdown', (e: any)=> this.fire(key, e)); // TODO: hint マウスカーソルを載せるとヒントをチップス表示する if (hArg.clickse) { this.cfg.searchPath(hArg.clickse, Config.EXT_SOUND); // 存在チェック em.on('pointerdown', ()=> { // clickse 効果音ファイル名 クリック時に効果音 const o: HArg = {fn: hArg.clickse, join: false}; if (hArg.clicksebuf) o.buf = hArg.clicksebuf; this.hTag.playse(o); }); } if (hArg.enterse) { this.cfg.searchPath(hArg.enterse, Config.EXT_SOUND); // 存在チェック em.on('pointerover', ()=> { // enterse 効果音ファイル名 ボタン上にマウスカーソルが載った時に効果音 const o: HArg = {fn: hArg.enterse, join: false}; if (hArg.entersebuf) o.buf = hArg.entersebuf; this.hTag.playse(o); }); } if (hArg.leavese) { this.cfg.searchPath(hArg.leavese, Config.EXT_SOUND); // 存在チェック em.on('pointerout', ()=> { // leavese 効果音ファイル名 ボタン上からマウスカーソルが外れた時に効果音 const o: HArg = {fn: hArg.leavese, join: false}; if (hArg.leavesebuf) o.buf = hArg.leavesebuf; this.hTag.playse(o); }); } if (hArg.onenter) { // マウス重なり(フォーカス取得)時、ラベルコール。必ず[return]で戻ること const key2 = key + hArg.onenter.toLowerCase(); const o: HArg = {fn: hArg.fn, label: hArg.onenter, call: true, key: key2}; if (glb) this.hGlobalEvt2Fnc[key2] = ()=>this.main.resumeByJumpOrCall(o); else this.hLocalEvt2Fnc[key2] = ()=> this.main.resumeByJumpOrCall(o); em.on('pointerover', (e: any)=> this.fire(key2, e)); } if (hArg.onleave) { // マウス外れ(フォーカス外れ)時、ラベルコール。必ず[return]で戻ること const key2 = key + hArg.onleave.toLowerCase(); const o: HArg = {fn: hArg.fn, label: hArg.onleave, call: true, key: key2}; if (glb) this.hGlobalEvt2Fnc[key2] = ()=>this.main.resumeByJumpOrCall(o); else this.hLocalEvt2Fnc[key2] = ()=> this.main.resumeByJumpOrCall(o); em.on('pointerout', (e: any)=> this.fire(key2, e)); } this.sndMng.loadAheadSnd(hArg); } waitCustomEvent(hArg: HArg, elc: EventListenerCtn, fnc: ()=> void) { this.goTxt(); if (! argChk_Boolean(hArg, 'canskip', true)) return; elc.add(window, 'pointerdown', (e: any)=> { e.stopPropagation(); this.fncCancelSkip(); fnc(); }); elc.add(window, 'keydown', (e: any)=> { //if (! e.isTrusted) return; if (e['isComposing']) return; // サポートしてない環境でもいける書き方 /* 限定する? this.hLocalEvt2Fnc['enter'] = fncKey; this.hLocalEvt2Fnc['arrowdown'] = fncKey; */ e.stopPropagation(); this.fncCancelSkip(); fnc(); }); this.waitCustomEvent4Wheel(elc, fnc); } private waitCustomEvent4Wheel = (_elc: EventListenerCtn, _fnc: ()=> void)=> {}; // イベントを全消去 private clear_event(hArg: HArg) { const glb = argChk_Boolean(hArg, 'global', false); const h = glb ?this.hGlobalEvt2Fnc :this.hLocalEvt2Fnc; for (const nm in h) this.clear_eventer(nm, h[nm]); if (glb) this.hGlobalEvt2Fnc = {}; else this.hLocalEvt2Fnc = {}; return false; } private clear_eventer(key: string, e2f: IEvt2Fnc) { if (key.slice(0, 4) !== 'dom=') return; for (const v of document.querySelectorAll(key.slice(4))) { v.removeEventListener('click', e2f); } } // イベントを予約 private event(hArg: HArg) { const KEY = hArg.key; if (! KEY) throw 'keyは必須です'; const key = KEY.toLowerCase(); const call = argChk_Boolean(hArg, 'call', false); const h = argChk_Boolean(hArg, 'global', false) ? this.hGlobalEvt2Fnc : this.hLocalEvt2Fnc; if (argChk_Boolean(hArg, 'del', false)) { if (hArg.fn || hArg.label || call) throw 'fn/label/callとdelは同時指定できません'; this.clear_eventer(KEY, h[key]); // その他・キーボードイベント delete h[key]; return false; } hArg.fn = hArg.fn || this.scrItr.scriptFn; // domイベント if (KEY.slice(0, 4) === 'dom=') { let elmlist: NodeListOf<HTMLElement>; const idx = KEY.indexOf(':'); let sel = ''; if (idx >= 0) { // key='dom=config:#ctrl2val const name = KEY.slice(4, idx); const frmnm = `const.sn.frm.${name}`; if (! this.val.getVal(`tmp:${frmnm}`, 0)) throw `HTML【${name}】が読み込まれていません`; const ifrm = document.getElementById(name) as HTMLIFrameElement; const win = ifrm.contentWindow!; sel = KEY.slice(idx +1); elmlist = win.document.querySelectorAll(sel); } else { sel = KEY.slice(4); elmlist = document.querySelectorAll(sel); } const need_err = argChk_Boolean(hArg, 'need_err', true); if (elmlist.length === 0 && need_err) throw `HTML内にセレクタ(${sel})に対応する要素が見つかりません。存在しない場合を許容するなら、need_err=false と指定してください`; const ie = elmlist[0] as HTMLInputElement; const type = (ie) ?ie.type :''; ((type === 'range' || type === 'checkbox' || type === 'text' || type === 'textarea') ? ['input', 'change'] : ['click']) .forEach(v=> { for (const elm of elmlist) this.elc.add(elm, v, e=> { const e2 = (elm as HTMLElement).dataset; for (const key in e2) { if (e2.hasOwnProperty(key)) this.val.setVal_Nochk('tmp', `sn.event.domdata.${key}`, e2[key]); } this.fire(KEY, e); }); }); // 押したまま部品外へ出たときも確定イベント発生 for (const elm of elmlist) this.elc.add(elm, 'mouseleave', e=> { if (e.which !== 1) return; this.fire(KEY, e); }); // return; // hGlobalEvt2Fnc(hLocalEvt2Fnc)登録もする } // その他・キーボードイベント h[key] = ()=> this.main.resumeByJumpOrCall(hArg); return false; } private goTxt = ()=> this.layMng.goTxt(); // 行末クリック待ち private l(hArg: HArg) { //traceDbg('[l]0 :'+ this.val.getVal('tmp:sn.skip.enabled'] +' :'+ (! this.val.getVal('tmp:sn.skip.all') +' isKidoku:'+ isKidoku); if (! this.val.getVal('tmp:sn.tagL.enabled')) {this.goTxt(); return false;} if (this.val.getVal('tmp:sn.skip.enabled') && ! this.val.getVal('tmp:sn.skip.all') && ! this.scrItr.isNextKidoku) { //traceDbg('[l] skip stop'); this.fncCancelSkip(); this.val.setVal_Nochk('tmp', 'sn.skip.enabled', false);// 次の選択肢(/未読)まで進むが有効か } if (this.val.getVal('tmp:sn.skip.enabled') && ('ps'.includes(String(this.val.getVal('sys:sn.skip.mode'))))) return false; if (this.val.getVal('tmp:sn.auto.enabled')) { //traceDbg('l:'+ (isKidoku?'既':'未') +' fn:'+ scriptFn +' idx:'+ idxToken +' cs:'+ vctCallStk.length); return this.wait({ time: Number(this.scrItr.isKidoku ? this.val.getVal('sys:sn.auto.msecLineWait_Kidoku') : this.val.getVal('sys:sn.auto.msecLineWait')) }); } if (argChk_Boolean(hArg, 'visible', true)) this.layMng.breakLine(); this.stdWait(()=> this.main.resume()); // stdWait()したらreturn true; return true; } // 改ページクリック待ち private p(hArg: HArg) { //traceDbg('[p]0 sk_en:'+ hTmp['sn.skip.enabled'] +' all:'+ (! hTmp['sn.skip.all']) +' isKidoku:'+ isKidoku +' mode:'+hSysVal['sn.skip.mode'] +' au_en:'+ hTmp['sn.auto.enabled'] +' au:'+ hTmp['sn.auto.enabled']); if (this.val.getVal('tmp:sn.skip.enabled') && ! this.val.getVal('tmp:sn.skip.all') && ! this.scrItr.isNextKidoku) { //traceDbg('[p] skip stop'); this.fncCancelSkip(); this.val.setVal_Nochk('tmp', 'sn.skip.enabled', false);// 次の選択肢(/未読)まで進むが有効か } if (this.val.getVal('tmp:sn.skip.enabled') && ('s' === String(this.val.getVal('sys:sn.skip.mode')))) {this.goTxt(); return false;} if (this.val.getVal('tmp:sn.auto.enabled')) { //traceDbg('p:'+ (isKidoku?'既':'未') +' fn:'+ scriptFn +' idx:'+ idxToken +' cs:'+ vctCallStk.length); return this.wait({ time: Number(this.scrItr.isKidoku ? this.val.getVal('sys:sn.auto.msecPageWait_Kidoku') : this.val.getVal('sys:sn.auto.msecPageWait')) }); } if (argChk_Boolean(hArg, 'visible', true)) this.layMng.breakPage(); this.stdWait( this.layMng.getCurrentTxtlayFore() && argChk_Boolean(hArg, 'er', false) ? ()=> {this.hTag.er(hArg); this.main.resume();} : ()=> this.main.resume() ); // stdWait()したらreturn true; return true; } // スキップ中断予約 private fncCancelSkip = ()=> {}; private set_cancel_skip() { this.fncCancelSkip = ()=> { this.fncCancelSkip = ()=> {}; this.val.setVal_Nochk('tmp', 'sn.tagL.enabled', true); // 頁末まで一気に読み進むか(l無視) this.val.setVal_Nochk('tmp', 'sn.skip.enabled', false);// 次の選択肢(/未読)まで進むが有効か this.val.setVal_Nochk('tmp', 'sn.auto.enabled', false);// 自動読みすすみモードかどうか this.layMng.setNormalWaitTxtLayer(); }; this.unregisterClickEvts(); return false; } private unregisterClickEvts() { const len = this.scrItr.lenCallStk; for (let i=0; i<len; ++i) { const cs = this.scrItr.getCallStk(i); if (! cs) continue; const hE1T = cs.hEvt1Time; if (! hE1T) continue; delete hE1T['Click']; delete hE1T['Enter']; delete hE1T['ArrowDown']; delete hE1T['wheel.y>0']; } } // ウェイトを入れる private wait(hArg: HArg) { this.val.saveKidoku(); const twSleep = new Tween.Tween(this) .to({}, uint(argChk_Num(hArg, 'time', NaN))) .onComplete(()=> this.main.resume()) .start(); this.stdWait(()=> twSleep.stop().end(), argChk_Boolean(hArg, 'canskip', true)); // stdWait()したらreturn true; return true; } isSkipKeyDown(): boolean { for (const v in this.hDownKeys) if (this.hDownKeys[v] === 2) return true; return false; } // 0:no push 1:one push 2:push repeating private readonly hDownKeys : {[name: string]: number} = { 'Alt' : 0, 'Meta' : 0, // COMMANDキー 'Control' : 0, 'ArrowDown' : 0, 'End' : 0, 'Enter' : 0, 'Escape' : 0, ' ' : 0, 'GoBack' : 0, // AndroidのBackキーだと思う } }