# sample09_hgimg4-yoko-WebAssembly.hsp

sample\sample_hgimg4-yoko-WebAssembly\ sample09_hgimg4-yoko-WebAssembly.hsp

    #include "hgimg4.as"

    #include "hsp3dish.as"
    #include "ezlocal-dish-js.hsp"
    ezLocalJS ,2  ; 引数 - p1:自動操作オプション、p2:自動チェックオプション

    #include "mod_ezphp.hsp"

    #include "mod_posteffect.as"
    // HSP3.7β10 から HSP3Dish.js でもポストエフェクトが使えるようになりました。
    #if __hspver__ >= 14090
    #define USE_EFFECT  ; ポストエフェクトを使用 : redraw → post_drawstart/post_drawend
    #endif

    #include "mod_vpad.as"

    title "HGIMG4 Sample"

    	;
    	;  HSP3同梱サンプル
    	;  sample\hgimg4\boxf_snake.hsp を改造
    	;
    	;	立方体を使った簡単なスネークゲーム
    	;	赤いブロックを取っていってください
    	;
    	randomize
    	myflag = -1
    	goto *restart

    *restart

    	score=0
    	level=1
    *gstart
    	gpreset
    	hspvpad_init 2,1+4			; バーチャルパッドの初期化

    #ifdef USE_EFFECT
    	post_reset
    	post_select POSTID_NONE
    	post_select POSTID_GLOW2        ; ポストエフェクト設定
    	post_setprm 80, 0.7, 6/*10*/    ; ポストエフェクトのパラメーター
    	                                ; - HSP3Dish.js ではパラメータによってはinput命令の描画がバグるようです。。。
    	                                ; - POSTID_GLOW2 だと第3パラメータ(level)が 7 以上だとバグります。
    #endif

    	gpresetlight 1,2			; ポイントライトを2個追加する

    	setcolor GPOBJ_LIGHT, 0.8,0.8,0.8	; 標準ライトカラーを設定
    	setdir GPOBJ_LIGHT, 0.2,0.2,0.2		; 標準アンビエントカラーを設定

    	;	nullライトとして設定します
    	gpnull id_model
    	gplight id_model, GPOBJ_LGTOPT_POINT,16,0.8,0.5	; ポイントライトとして設定する
    	setcolor id_model, 1,1,1			; ライトカラーを設定
    	gpuselight id_model				; ポイントライトを使用する

    	;	nullライトとして設定します
    	gpnull id_target
    	gplight id_target, GPOBJ_LGTOPT_POINT,16,1,0.5	; ポイントライトとして設定する
    	setcolor id_target, 1,0,0			; ライトカラーを設定
    	gpuselight id_target, 1				; ポイントライトを使用する

    	wx=30:wz=30				; フィールドのサイズ
    	basex=0.5-(wx/2):basez=0.5-(wz/2)	; ベース座標
    	basey=0.5				; Y座標のベース
    	dim map,wx,wz				; 仮想マップ
    	dim boxcolor,8				; 色リスト

    	;	色リストを設定
    	boxcolor(1) = $c0c0c0
    	boxcolor(2) = $a00000
    	boxcolor(3) = $a060

    	;	方向リストを作成
    	dim dirpx,4:dim dirpz,4:sdim dirstr,64,4
    	dirpz(0)=-1:dirpz(2)=1
    	dirpx(1)=1:dirpx(3)=-1
    	dirstr(0)="↑"
    	dirstr(1)="→"
    	dirstr(2)="↓"
    	dirstr(3)="←"

    	;	壁を作成
    	col_wall = $808080
    	gpbox i, 1, col_wall	; 箱ノードを生成する
    	setscale i, wx, 1, 1 : setpos i, 0 , basey , basez
    	gpbox i, 1, col_wall	; 箱ノードを生成する
    	setscale i, wx, 1, 1 : setpos i, 0 , basey , basez+wz-1
    	gpbox i, 1, col_wall	; 箱ノードを生成する
    	setscale i, 1, 1, wz : setpos i, basex , basey , 0
    	gpbox i, 1, col_wall	; 箱ノードを生成する
    	setscale i, 1, 1, wz : setpos i, basex+wx-1 , basey , 0
    	repeat wx : map(cnt,0)=1 : map(cnt,wz-1)=1 : loop
    	repeat wz : map(0,cnt)=1 : map(wx-1,cnt)=1 : loop

    	histmax=64:curhist=8+level
    	dim myid, histmax
    	dim myposx, histmax
    	dim myposz, histmax
    	myx=wx/2:myz=wz/2
    	if myflag > 0 : myflag=0
    	mykey=2
    	mydir=0
    	mydist=10.0
    	myspeed=15
    	left=4+level
    	gosub *move_player

    	max=level*10+10
    	repeat max
    		gosub *rndaxis
    		a=1:gosub *putbox
    	loop
    	gosub *add_target

    	gpfloor id_floor, wx,wz, $606060	; 床ノードを追加

    	gosub *camera_init
    	frame=1
    	hval=0
    	if myflag == -1 {
    		msg="tap to start!"
    		repeat
    			gosub *key_input
    			gosub *camera_update
    			gosub *screen_update
    			if key & 256 : break
    		loop
    		myflag = 0
    	}
    	msg="Level..."+level
    	repeat 120
    		gosub *key_input
    		gosub *camera_update
    		gosub *screen_update
    	loop
    	msg=""
    *main
    	;	キー入力
    	gosub *key_input
    	if (frame\myspeed)=0 {
    		; プレイヤーを更新
    		gosub *move_player
    	}
    	if (frame\240)=0 {
    		gosub *speedup
    	}
    	gosub *camera_update
    	gosub *screen_update
    	if myflag>0 : goto *main_ov
    	if left=0 : goto *main_clr
    	goto *main

    *main_ov
    	;	ゲームオーバー
    	if myflag == 1 {
    		msg="Crash...!"
    		// ゲームオーバー ループ初回
    		gosub *gameover_1
    		myflag = 2
    	}
    	if myflag == 2 {
    		// ユーザー名入力待ちループ
    		gosub *gameover_2
    	}
    	stick key
    	if key&128 : goto *byebye
    	;if key&$130 : goto *restart
    	gosub *camera_update2
    	gosub *screen_update
    	goto *main_ov

    *screen_gameovermes_update
    	if myflag == 2 {
    		// ユーザー名入力待ちループ
    		gosub *gameover_2_mes
    	}
    	if myflag == 3 {
    		// スコアsend後のループ
    		gosub *gameover_3_mes
    	}
    	return

    *main_clr
    	;	クリア
    	msg="Clear...!"
    	repeat 120
    	stick key
    	if key&128 : goto *byebye
    	gosub *camera_update2
    	gosub *screen_update
    	loop
    	level++
    	goto *gstart

    *byebye
    	end

    *screen_update
    	;	描画処理
    #ifdef USE_EFFECT
    	post_drawstart			; 描画開始
    #else
    	redraw 0
    #endif

    	;	ライトの色を変化させる
    	;	hsvcolorの値をRGB値に変換する
    	hsvcolor hval,255,255
    	hval+:if hval>192 : hval=0
    	rval=double(ginfo_r)/255.0
    	gval=double(ginfo_g)/255.0
    	bval=double(ginfo_b)/255.0
    	setcolor id_model, rval,gval,bval	; ライトカラーを設定

    	rgbcolor $305070:boxf 0,0,1040,480		; 背景クリア
    	gpdraw				; シーンの描画
    	gmode 0
    	font "",20
    	color 255,255,255
    	pos 28,8:mes "SCORE:"+score
    	if msg!="" {
    		font "",120,,4 : pos 210,210
    		if msg == "tap to start!" : font "",90,,4 : pos 270,210
    		; - fontsystemの影響で(?)文字の描画サイズがあまり大きくなるとhtmlが全体(fontsystem込み)を表示しようと縮小してしまう。
    		;   結果、ゲーム画面の右側に余白ができてしまうので大きさを制限する必要がある。
    		mes msg,mesopt_outline
    	}
    	gosub *screen_gameovermes_update

    	hspvpad_put mykey

    #ifdef USE_EFFECT
    	post_drawend			; 描画終了
    #else
    	redraw 1
    #endif
    	await 1000/60			; 待ち時間
    	frame++
    	return

    *rndaxis
    	;	ランダムな位置を取得->(x,y)
    	x=rnd(wx-2)+1:z=rnd(wz-2)+1
    	if map(x,z) : goto *rndaxis
    	if abs(myx-x)+abs(myz-z)<4 : goto *rndaxis
    	return
    *putbox
    	;	箱を設置
    	;	(x,z)の位置にboxを設置、aの値が色リストのコード
    	map(x,z)=a
    	gpbox i, 1, boxcolor(a)	; 箱ノードを生成する
    	setpos i, basex+x, basey, basez+z
    	return

    *add_target
    	;	ターゲットを設置
    	gosub *rndaxis
    	a=map(x+1,z)+map(x-1,z)+map(x,z+1)+map(x,z-1)
    	if a>=2 : goto *add_target

    	a=2:gosub *putbox:redid=i
    	tarx=basex+x:tarz=basez+z
    	setpos id_target,tarx,1,tarz
    	return

    *key_input
    	;	キー入力処理
    	stick key,$100
    	hspvpad_key key

    	if key&128 : goto *byebye
    	if key&2 : mydir=0 : mykey=2
    	if key&8 : mydir=2 : mykey=8
    	if key&1 : mydir=3 : mykey=1
    	if key&4 : mydir=1 : mykey=4
    	return

    *move_player
    	;	プレイヤーを進める
    	if myflag>0 : return
    	if curhist>0 {
    		i=curhist
    		if myid(i)>0 {
    			map(myposx(i),myposz(i))=0
    			delobj myid(i)
    		}
    		repeat
    			if i=0 : break
    			myid(i)=myid(i-1)
    			myposx(i)=myposx(i-1)
    			myposz(i)=myposz(i-1)
    			i--
    		loop
    	}
    	myx+=dirpx(mydir)
    	myz+=dirpz(mydir)
    	a=map(myx,myz)
    	if a=1|a=3 {				; やられた?
    		myflag=1
    		return
    	}
    	if a=2 {				; ターゲット
    		gosub *add_player
    		delobj redid
    		score+=level*10
    		left--
    		if left>0 : gosub *add_target
    	}
    	x=myx:z=myz:a=3:gosub *putbox
    	newid=i
    	realx=basex+x:realz=basez+z		; 先頭のプレイヤー座標
    	setpos id_model,realx,1,realz

    	myid(0)=newid
    	myposx(0)=x
    	myposz(0)=z

    	return
    *add_player
    	;	プレイヤーを伸ばす
    	curhist++
    	if curhist>=histmax : curhist=histmax-1
    	return
    *speedup
    	;	プレイヤーのスピードアップ
    	if myspeed>4 : myspeed--
    	return

    *camera_init
    	;	カメラ初期化
    	camx=0.0:camy=15.0:camz=0.0
    	camadjz=13.0:camyfix=0
    	mydist=camadjz
    	camang = 0.0
    	return

    *camera_update
    	;	カメラ設定サブルーチン
    	dx = realx-tarx
    	dz = realz-tarz
    	if myflag>=0 {
    		todist = sqrt(dx*dx+dz*dz)*0.5+5
    		mydist+=(todist-mydist)*0.01
    	}

    	dx=(realx-camx)*0.02
    	dz=(realz-camz)*0.02
    	camx+=dx:camz+=dz
    	camadjz = 5 + mydist
    	setpos GPOBJ_CAMERA, camx,camy,camz+mydist		; カメラ位置を設定
    	gplookat GPOBJ_CAMERA, camx,camyfix,camz		; カメラから指定した座標を見る
    	return

    *camera_update2
    	;	カメラ設定サブルーチン(周回)
    	dx=sin(camang)*mydist
    	dz=cos(camang)*mydist
    	if camy>3 : camy-=0.05
    	setpos GPOBJ_CAMERA, camx+dx,camy,camz+dz		; カメラ位置を設定
    	gplookat GPOBJ_CAMERA, camx,camyfix,camz		; カメラから指定した座標を見る
    	camang+=0.01
    	return

    *gameover_1
    	your_name = ""

    	// 既に自分の名前があるか確認
    	exist "save/myname.txt"
    	if strsize != -1 {
    		notesel your_name
    		noteload "save/myname.txt"
    		noteunsel
    	}
    	// HSP3Dish.js で input は 闇なので独自のインターフェイスを用意した方が良いと思う。。。
    	objsize 280,32
    	pos 370,100 : input your_name : id_input_yourName = stat
    	pxy = ginfo_cx, ginfo_cy
    	pos pxy(0),pxy(1) //: mes "※ 英数記号のみ"
    	objsize 80,32
    	pos 670,100 : button gosub "送信", *send
    	// スマホでキーボードを出させる苦肉の策(要素を作成)
    	sdim strbuf, 1024  ; Androidでinput入力を捕捉する必要があるため
    	sdim mae_strbuf, 1024
    	your_name_ = your_name
    	strrep your_name_, "'", "\\'"  ; エスケープ

    	inputSizeX = 280
    	inputSizeY = 32
    	inputPosX = 370
    	inputPosY = 100
    	exec {"
    		// Androidはinput内容を捕捉する必要がある
    		var mae_hsp_input_value = '"}+your_name_+{"';

            if (navigator.userAgent.match(/iPhone|iPad|Android/)) {
    			if(document.getElementById('hsp_dummy') === null){
    				// キーボード表示用のinput作成(上部に極透明なオブジェクトを作成)
    				const inputElement = document.createElement('input');
    				inputElement.id = 'hsp_input';
    				inputElement.type = 'text';
    				inputElement.inputmode = 'email';
    				inputElement.style.position = 'fixed';
    				inputElement.style.left = '0';
    				inputElement.style.top = '0';
    				inputElement.style.fontSize = '100px';
    				inputElement.style.display = 'none';
    				inputElement.style.zIndex = -1;
        		    if (navigator.userAgent.match(/Android/)) {
    					inputElement.value = mae_hsp_input_value;
    					inputElement.style.width = '98%';
    					inputElement.style.height = '100px';
    					inputElement.style.opacity = '.2';
    				}else{
    					inputElement.style.width = '1px';
    					inputElement.style.height = '1px';
    					inputElement.style.opacity = '.01';
    				}
    				// フォーカスアウトでinputを非表示にして、dummyを表示する
    				inputElement.addEventListener('focusout', (event) => {
    					document.getElementById('hsp_input').style.display = 'none';
    					document.getElementById('hsp_dummy').style.display = '';
    				});
    				// Androidはinput内容を捕捉する必要がある
    				if (navigator.userAgent.match(/Android/)) {
    					inputElement.addEventListener('input', (event) => {
    						if(!event.target.value.match(/^[a-zA-Z0-9!-/:-@\[-`{-~ ]*$/)){
    							// 半角英数以外は入力禁止(他デバイスに合わせてる)
    							event.target.value = mae_hsp_input_value;
    						}
    						mae_hsp_input_value = event.target.value;
    						stringToUTF8Array(mae_hsp_input_value, Module.HEAP8, "} + varptr(strbuf) + {", 1024);
    					});
    				}
    				document.body.appendChild(inputElement);

    				// click->focus()のためのダミーElement
    				const dummyElement = document.createElement('div');
    				dummyElement.id = 'hsp_dummy';
    				dummyElement.style.position = 'absolute';
    				dummyElement.style.fontSize = '100px';
    				SetRectDummyDiv(dummyElement);
    				dummyElement.style.opacity = '.01';
    				//dummyElement.style.opacity = '.5';           // test用
    				//dummyElement.style.backgroundColor = 'red';  // test用
    				dummyElement.addEventListener('click', () => {
    					// スマホはタッチ系のアクション内じゃないとfocus()効かない
    					document.getElementById('hsp_input').style.display = '';
    					document.getElementById('hsp_dummy').style.display = 'none';
    					document.getElementById("hsp_input").focus();
    				});
    				document.body.appendChild(dummyElement);
    				// ダミーElementのリサイズ
    				function SetRectDummyDiv(erement){
    					if (erement === null) return;
    					const inputSizeX = "} + inputSizeX + {";
    					const inputSizeY = "} + inputSizeY + {";
    					const inputPosX = "} + inputPosX + {";
    					const inputPosY = "} + inputPosY + {";
    					const wx = ENV.HSP_WX;  // 480
    					const wy = ENV.HSP_WY;  // 800
    					const sx = ENV.HSP_SX;  // String(window.innerWidth)
    					const sy = ENV.HSP_SY;  // String(window.innerHeight)
    					const rect = document.getElementById("canvas").getBoundingClientRect();
    					if( (sx/sy) < (wx/wy) ){
    						// 横幅に余白がなく縦方向に余白があるとき
    						const rate = wy / wx * sx / sy;  // ディスプレイ高さに対してゲーム画面高さがどのくらいか
    						erement.style.left = rect.left + window.pageXOffset + rect.width*(inputPosX/wx)+'px';
    						erement.style.width = rect.width*(inputSizeX/wx)+'px';
    						erement.style.top = rect.top + window.pageYOffset + rect.height*((1-rate)/2 + (inputPosY/wy) * rate)+'px';
    						erement.style.height = rect.height*((inputSizeY/wy) * rate)+'px';
    					}else {
    						// 縦方向に余白がなく横幅に余白があるとき
    						const rate = wx / wy * sy / sx;  // ディスプレイ幅に対してゲーム画面幅がどのくらいか
    						erement.style.left = rect.left + window.pageXOffset + rect.width*((1-rate)/2 + (inputPosX/wx) * rate)+'px';
    						erement.style.width = rect.width*((inputSizeX/wx) * rate)+'px';
    						erement.style.top = rect.top + window.pageYOffset + rect.height*(inputPosY/wy)+'px';
    						erement.style.height = rect.height*(inputSizeY/wy)+'px';
    					}
    				}
    				// デバイス回転時にダミーElementをリサイズ
    				function DeviceOrientation_InputDummy() {
    					setTimeout(() => {
    						SetRectDummyDiv(document.getElementById('hsp_dummy'));
    					}, 100);
    				}
    				window.removeEventListener('orientationchange', DeviceOrientation_InputDummy);
    				window.addEventListener('orientationchange', DeviceOrientation_InputDummy);
    			}else{
    				document.getElementById('hsp_dummy').style.display = '';
    			}
    		}
    	"}
    	objsel id_input_yourName
    	return

    *gameover_2
    	mtlist touchid            ; マルチタッチ
    	if stat {
    		// input部以外タッチでフォーカス外す
    		exec {"
    			if(document.getElementById("hsp_input")){
    				document.getElementById("hsp_input").blur();
    			}
    		"}
    	}
    	if mae_strbuf != strbuf {
    		// Androidではinput入力を捕捉する必要があるため
    		mae_strbuf = strbuf
    		your_name = strbuf
    		objprm id_input_yourName, your_name
    	}
    	return
    *gameover_2_mes
    	color 255,255,255 : font "", 20
    	pos 230,100:mes "YOUR NAME : "
    	pos pxy(0),pxy(1) : mes "※ 英数記号のみ"
    	return

    // データ保存
    *send
    	clrobj id_input_yourName, id_input_yourName+1  ; inputとbuttonを削除
    	; ↑ 引数なしで全てのオブジェクトを消すとmod_posteffect.as内で作られるlayerobjも消えてしまうので注意

    	// スマホでキーボードを出させる苦肉の策(要素を削除)
    	exec {"
    		if(document.getElementById('hsp_input')){
    			document.getElementById('hsp_input').remove();
    		}
    		if(document.getElementById('hsp_dummy')){
    			document.getElementById('hsp_dummy').remove();
    		}
    	"}
    	if your_name == "" : your_name = "NO NAME"
    	// 自分の名前を登録
    	notesel your_name
    	notesave "save/myname.txt"
    	noteunsel
    	devcontrol "syncfs"

    	// ユニークID生成(テキトウ20桁)
    	exist "save/uniqid.txt"
    	if strsize == -1 {
    		// まだブラウザに保存されていなかったとき
    		uniqId = TekitouUniqId(20)  ; 20桁のUniqueID生成
    		dirlist dummy,"save",5 : if stat == 0 : mkdir "save"  ; ← Windowsアプリとして実行するとき用に
    		notesel uniqId
    		notesave "save/uniqid.txt"  ; ブラウザに保存
    		devcontrol "syncfs"
    		noteunsel
    	}else {
    		// すでにブラウザにUniqueIDが保存されていたとき。読み込み
    		notesel uniqId
    		noteload "save/uniqid.txt"
    		noteunsel
    	}

    	// データベースファイル「sample.db」を開き(ファイルが無ければ作られる)、「UserScore」テーブルを作成 (すでにテーブルがあれば作成されません)
    	// - 基本的にデータベースファイルは事前に準備しておきhtmlファイル等と共にアップロードしてください。
    	ezphpsql "sample.db", "CREATE TABLE IF NOT EXISTS UserScore (ID INTEGER PRIMARY KEY, uniqid TEXT, score INTEGER, name TEXT, UNiQUE(uniqid))"
    	// とりあえずスコア0でユーザーデータを新規挿入。ただしUNiQUE制約があるので既に同じuniqidがある(ゲームを遊んだことがある)場合はErrorが返る(stat=0、がそれは無視してOK)
    	ezphpsql "sample.db", "INSERT INTO UserScore (uniqid, score, name) VALUES (?, ?, ?)", uniqId, 0, your_name  ; 第3引数以降はプレースホルダにセットする値で「?」の位置に前から順で割り当てられます
    	// nameは常に更新。scoreは高い時だけ更新。uniqidを目印にして。
    	query = {"
    		UPDATE UserScore
    		SET
    			name = ?,
    			score = CASE
    				WHEN score < ? THEN ?
    				ELSE score
    			END
    		WHERE uniqid = ?;
    	"}
    	ezphpsql "sample.db", query, your_name, score, score, uniqId  ; ezphpsqlは中身が#defineなので第2パラメータまでのときでも直接複数行文字列を置くことはできません。
    	// score降順ですべてのユーザー分取り出す(単純な平文出力。jsonで出力したい場合はezphpsql_jsonを使ってください。)
    	ezphpsql "sample.db", "SELECT uniqid, score, name FROM UserScore ORDER BY score DESC"
    	rankText = refstr  ; シンプルなSELECT文なら平文で受け取って処理すると良いでしょう。(データ内の改行は「<BR>」で置換して出力されます)
    	notesel rankText
    	myflag = 3
    	msg = ""
    	objsize 150,100
    	pos 50,50 : button "再チャレンジ", *pre_restart
    	return
    *gameover_3_mes
    	// ランキング描画
    	pos 250, 50 : font "", 25
    	maeScore = -1 : maeJunni = 1
    	repeat notemax / 3  ; 自分が何項目要求したかで何人分返ってきたかを計算。(ここでは uniqid, score, name の3項目要求している)
    		noteget rank_uniqid, cnt*3 : strrep rank_uniqid, "uniqid:", ""  ; keyが邪魔なので取り除く
    		noteget rank_score, cnt*3+1	: strrep rank_score, "score:","" : rank_score = int(rank_score)
    		noteget rank_name, cnt*3+2 : strrep rank_name, "name:", ""
    		if rank_uniqid == uniqId : color 255,128,128 : rank_uniqid = " ("+rank_uniqid+")" : else :rank_uniqid = "" : color 255,255,255  ; 自分だけUniqID表示
    		junni = (cnt+1) : if maeScore == rank_score : junni = maeJunni : else : maeJunni = junni : maeScore = rank_score  ; 同スコアは同率順位
    		mes ""+junni+"位  "+rank_score+" - "+ rank_name + rank_uniqid
    	loop
    	return

    *pre_restart
    	clrobj
    	goto *restart
    	stop

    // テキトウに N桁 Unique な ID を生成する関数
    #module
    #defcfunc TekitouUniqId int keta
    	randomize
    	uniqId = ""
    	repeat keta
    		uniqid += "_"
    		id = rnd(10+26+26)
    		if id >= 36 : poke uniqId, cnt, id+61 : continue  ; a-z
    		if id >= 10 : poke uniqId, cnt, id+55 : continue  ; A-Z
    		poke uniqId, cnt, id+48  ; 0-9
    	loop
    	return uniqId
    #global