2025年9月7日日曜日

LOMの全イベント管理シミュレーターを作った話

LOM全イベントの完全管理 

前回構想だけ話しましたが結局作りました
LOMでは全68イベントが存在します。

実の所イベントだけなら(固定の初期範囲と初期位置)だけならそこまで複雑になりません。
所がシミュレーションを好きなランドメイクをしつつイベントも好きな様に組むとなると爆発的に難しくなります。

個々の仕組みはそんなに難しくないのですがこれが組み合わさってそれをシステムに落とすとなると難易度がヤバい(語彙力低下)。
でももっと深く遊ぶのにランドメイクのシミュレーションだけではイベントを別途メモ帳とか別の場所で管理する必要があって面倒臭い。
そのためページ1つで完結させたくなった。

ファイル構成を考える

今回は処理とデータを分離しました。
とは言え、DBの使えるサーバーではないのでDB用JSを作ってそれをインポートで引っ張ります。
仕様が変更されることが無いのでシステム的な拡張性を考慮する必要がないので名前付きDBのような仕組みは不要で、欲しいのは配列の塊だけです。

今回は普通のjavascriptじゃなくてmjsで作成しました。
拡張子をmjsにしようと思ったら、サーバーが対応してないという根本的な問題があった(サーバー側がファイル形式をホワイトリストで処理してるようでアクセスが出来なくなる)のですがそもそもmjsは拡張子jsでも動くので結局拡張子はjsにする事になりました。

さておき、構成としては
・メイン処理JS
・DOM用オリジナルラッパーJS
・DB-JS
この3つ。
検索時に静的に埋め込まれていた方が良いかなという視点でAFとイベント名のリストはHTML側に記述しています。管理面を考えると動的に生成した方が楽なんだけどSEO的にはどっちが良いのか未だ分からない。最近のクローラーは動的な内容もある程度判定するようなんだけどベタ記述されてれば確実に読み込みはするはず。

操作部分を静的にするとデメリットもあって、描画を待たないといけない。
対処方法は3つ
・body下でjavascriptを読み込ませる
・window.onloadで発火させる
・DOMContentLoaded で発火させる

body下の場合htmlは上から順に処理されるのでボタンやリストのテーブルなどの描画が終わった後にjavascriptが実行されます(必要最低限の描画直後で最速だが、場合によっては描画が変な遅れ方するとエラーする可能性はある)。

onloadはスクリプト含めた描画に必要な全てを読み込んだ後に実行されます(一番確実だが一番遅い)

DOMContentLoaded はHTML の文書が完全に読み込まれ構文解析され、すべての遅延スクリプト(deferの遅延読み込み、module読み込み)がダウンロードされ、実行されたときに発火。
※画像、サブフレーム、非同期スクリプトは待たない。

DOM用オリジナルラッパーJSは過去に紹介しているbscラッパーです。
elementをラップして短縮記述出来ます。
例えば、document.getElementById()は$i()に圧縮出来ます。

処理を考える

ランドメイクのアンドゥをする際に前回のものは全工程を再処理してたので遅いのが少し不満だったため今回は各工程をスナップショットする様に変更。これによって今回はアンドゥがだいぶ高速化出来ています。

1.初期化

■初期値1(初回ロード)
・マップカラー、全マップ、初期マップインデックスを設定。
・イベントリスナーを設定。
・ログを初期化し、アーティファクト、6x6エリア、6x6マップをセットアップ。
・URLにクエリがある場合、保存状態を復元し、ない場合はデフォルト。

全体のマップの色や1度だけの処理部分。初期化2も呼ぶ。

■初期化2(随時リセット)
・イベントを初期化(非表示、無効、チェック解除)。
・イベント状態を0で初期化(68要素)。
・保存スナップショットを初期化(可変)。
・アーティファクトを初期化(26要素)。
・6x6エリアのマナを初期化し、初期マナ値を設定。
・ログに初期マップインデックスを記録。

エリアの変更等で呼ばれる
リセット用の初期化

2.操作

順序的には
マップ処理→配置処理→イベント処理
マップが無ければランドが配置できずランドが無ければイベントをする場所がない
大枠はこの3工程をループ

AFには基本イベントがある。
街以外はランドロックの解除が必要なので基本1つはイベントがある。
AF配置でも、イベント終了でもイベントが解除される

■初期操作

・25x25の全体マップから初期エリアの選択→6x6エリア更新
・ポスト取得
・ポストの配置(ポストだけは陸なら何処でも置けるため専用の配置)
・ドミナの取得(以降隣接配置に変わる)

■AF

・アーティファクトを選択し、配置可能なエリアを表示。
・指定されたAFを配置可能な6x6エリアを計算し、ハイライト。
・キャンセル時はハイライトとAFを戻す

・配置→位置の記憶→マナ計算(周囲4マスの変更)→イベント解放
・町の場合無条件でランドロック解除

■イベント

・イベントの開始
・まいごのプリンセス等は開始後にAFを取得
・イベントの終了
・ランドロック解除
・次イベント解放

■例外

・果樹園の60個採取は単独判定
・丙子椒林剣をサブイベント処理

■失敗・消滅
・失敗はその時点でゲーム内でも失敗が表示される
・消滅は条件が満たせなかった場合で結果的に発生しない

■ステップ

・ステップが変動するたびに処理
・各変数を全て保存
・リドゥ、アンドゥで変数を復元
・ステップ数上限を超える場合は変数保存、以内であれば復元
・状態をリアルタイムで復元用url生成

■その他描画関係

シミュレーションに関係ない部分の描画
・リストの表示切替等
・全体マップの表示切替等
・復元urlの𝕏でのポスト機能

3.処理の関数化

実処理はもう少し細分化。関数ベース。

関数名 処理内容
setdata ページロード時の初期化。マップカラー、全マップ、初期マップインデックス(gmi=97)を設定。リスナーを設定し、ログ、AF、6x6エリア/マップを初期化。URLクエリがあれば状態を復元、なければlms()を呼び出す。
setstartaf AF、イベント、マナ、ログの初期状態を設定。イベントを非表示/無効化、gevsを0で初期化、AF配列(gafo, gafc, gafs)を初期化、6x6エリアのマナ(l36m, l36a)を設定、ログに初期マップを記録。
set6x6area 6x6マップの表示位置を調整。マップインデックスから座標を取得し、box6x6のleft/topをピクセル単位で設定。
set6x6map 6x6マップを表示。マップカラーと6x6マップデータを取得し、各セル(box65)の背景色を設定。
get6x6map 6x6マップデータを生成。有効ポイントとフルマップから6x6領域を切り出し、山データを適用、座標ラベルを設定してデータを返す。
set6x6coordinates 6x6マップの座標ラベル(行:数値、列:アルファベット)を設定。インデックスを座標に変換して表示。
m6x6del 6x6エリアとマナ表示をクリア。area36とmana36のinnerHTMLを空にする。
get6x6mana 6x6マップのマナデータを取得。有効ポイントとマナマップから6x6領域のマナを抽出。
setfullmap 25x25の全体マップを表示。マップカラーとフルマップデータを用いて各セル(box25)の背景色を設定。
highlightmap 25x25マップにハイライトを適用。ハイライト用マップデータで背景色を更新。
boxclick 25x25マップのセルクリック処理。クリックされたインデックスが有効ポイントに含まれる場合、setmapを呼び出してマップを切り替え。
setmap 指定されたマップインデックスに切り替え。mapselectを更新し、6x6座標を設定後、lms()を呼び出す。
mapmove マップを上下左右に移動。方向(左/右/上/下)に応じてインデックスを変更し、lms()を呼び出す。
choiceaf AFを選択し、配置可能なエリアを表示。選択したAFのボタンを非表示、情報を表示、配置可能エリアをハイライト。
availableareas AFを配置可能な6x6エリアを計算。地形条件と隣接AFの状態を基に、配置可能なセルを透明度0.5でハイライト。
m6x6p100 6x6エリアの透明度をリセット(1.0に設定)。
afcancel AF選択をキャンセル。AF情報/ステータスを非表示、AFボタンを再表示、透明度をリセット。
setaf 6x6マップの指定位置にAFを配置。配置可能かチェックし、ログ、AF状態、エリア、マナを更新。必要に応じてAF解放や自動アンロックを実行し、lms()を呼び出す。
setafdirect 指定されたAFを指定位置に直接配置。UI操作を介さず、setafと同様の処理を行い、lms()を呼び出す。
getbuildingblocks AF「ビルディングブロック」を解放。gafs[1]=1を設定し、ボタンを表示。
checkmana AF配置時のマナを計算。隣接セルのマナを加算し、上限3に制限。隣接セルのマナも更新。
defaultunlock 特定ランド(街など)を自動アンロック。固定リストに基づき、gafs[n]=3を設定。
autounlock ランドのアンロックイベントを自動終了。アンロックリストからイベント番号を取得し、eventclearを呼び出す。
getaf 指定されたAFを解放。未解放の場合、gafs[n]=1を設定し、ボタンを表示。
manazero 特定ランド(Lucemia)のマナをゼロにリセット。l36mを更新し、UIに反映。
eventstart イベントを開始。gevs[n]=2を設定、UIを更新(チェックボックス有効、背景白)。競合イベントをチェックし、ログを記録、lms()を呼び出す。
eventclear イベントをクリア。開始状態を確認し、gevs[n]=3を設定、UIを更新(背景緑)。関連AFやアンロックイベントを実行、ログを記録、lms()を呼び出す。
subevent1 サブイベント(果実60個)を処理。チェックボックスを更新、AFを解放、ログを記録、lms()を呼び出す。
subevent2 サブイベント(武器「丙子椒林剣」)を処理。チェックボックスを更新、ログを記録、lms()を呼び出す。
eventcheck イベントの解放条件をチェック。AF、マナ、イベント状態を基に開始可能イベントを判定、UIを更新。進行不能イベントを失敗状態(gevs=4)に設定。
eventdisplay イベントリストの表示を制御。ラジオボタンの選択に応じて、開始済み/進行中/全イベントを表示/非表示。サブイベントの表示も制御。
relogw ログを圧縮(sX-eXをwXに変換)。glog[0]を圧縮し、glog[1]に保存。
compressLog ログ文字列を圧縮。sX-eXをwXに置換。
logconvert ログをHTML形式に変換。AF配置、イベント開始/終了、サブイベントをリスト形式で表示。最後のログをlaststepに表示。
log2txt ログエントリをテキストに変換。AF配置、イベント、サブイベントの内容を文字列化。
savestep 状態を保存し、URLを更新。ログと履歴を比較し、スナップショットを保存、URLに圧縮ログを付加、ステップ数を更新。
savesnapshot 現在のゲーム状態をスナップショットとして保存。グローバル変数とUI状態を保存。
loadset URLや履歴から状態を復元。ログをパースし、AF配置、イベント、サブイベントを再現、lms()を呼び出す。
undo 1ステップ前の状態に復元。ログの最後のエントリを削除し、スナップショットを復元。
redo 1ステップ後の状態に復元。履歴の次のエントリを適用。
undoall 初期状態(ステップ1)に復元。スナップショット1を復元。
redoall 最新の状態に復元。履歴の最終ステップを復元。
restoreSnapshot 指定されたスナップショットを復元。グローバル変数とUI状態を復元し、lms()を呼び出す。
add1step 1ステップ分のログを適用。AF配置、イベント、サブイベントを再現。
makecheck チェックリストを更新。特定ランドのマナや配置状況をチェックし、条件達成(OK)を表示。ランドレベルを計算。
convertlandlevel ランドのレベルを計算。基準点(AF0)とのマンハッタン距離と配置数を加算。
mana2tag マナ配列をHTML形式(色付きスパン)に変換。マナ値に応じて背景色を設定。
setcolor4 マップカラーの説明を表示。マップカラーを取得し、color4要素に背景色を設定。
openmyurl 保存URLを新しいタブで開く。saveurlの値をwindow.openで開く。
urlpost 進行状況をXに投稿。チェックリストを基にテキストを生成し、xpostで投稿。
setlistener イベントリスナーを設定。マップクリック、AF選択、イベント操作、Undo/Redo、表示切り替えなどのリスナーを登録。
iddisplayswitch 指定された要素の表示/非表示を切り替え。displayスタイルをトグル。
arrayaddition マナ配列を要素ごとに加算。
arrayaddition3max マナ配列を加算し、上限3に制限。
index2coordinates インデックスを座標(行, 列)に変換。

こんな感じ。

2025年8月25日月曜日

VBSでUTF-8で読み書きする

■ ADODB.StreamのUTF-8読み書き

VBS自体は使えなくなるかもしれないんですがVBAや他の言語でも使えるので一応備忘
基本的にテキストの読み書きはFileSystemObjectを使うのですがShift-JISしか使えません。

今回はWebページのヘッダーのメタタグの機械的な変換をしたいなぁと思った時にWebページは全部UTF-8に移行済みだったので、じゃあ「ADODB.Stream」という事ですね。

ADODB.Streamで出来る事は主に2つ、文字コード指定してのテキストの読み書きとバイナリデータの読み書きです。

■UTF-8を読み込む

まずは読み込みから
htmlpath="c:\makewebsite\test.html"

With CreateObject("ADODB.Stream"):.Type = 2:.Charset = "UTF-8":.Open:.LoadFromFile htmlpath:html = .ReadText:.Close:End With

  1. 通常通りCreateObjectでオブジェクトを作ります。
  2. Typeは1がバイナリモード、2がテキストモードです。
  3. Charsetは文字コード、今回はUTF-8のhtmlを読み込むので UTF-8です。
  4. Openでストリームを開きます
  5. LoadFromFile にファイルパスを指定
  6. ReadTextでhtmlの内容を全て取得
  7. Closeでストリームを閉じます

■UTF-8で書き込み①

次に書き込み
htmlpath="c:\makewebsite\test.html"
retext="書き込む文字"

With CreateObject("ADODB.Stream"):.Type = 2:.Charset = "UTF-8" :.Open:.WriteText retext:.SaveToFile htmlpath, 2:.Close:End With

これでOK!単純に先ほど逆順に処理します

  1. CreateObject(ストリームオブジェクト)
  2. Type(2:テキストモード)
  3. Charset(文字コード:UTF-8)
  4. Openでストリームを開きます
  5. WriteText でストリームに書き込む
  6. SaveToFile でストリームを全て出力、2は上書き
  7. Closeでストリームを閉じます

■Byte Order Mark/バイトオーダーマーク

ただし、1つだけ問題があります。
ADODB.Streamの仕様上書き込みにはBOM(ばいとおーだーまーく)が入ります
これが何かというとファイル先頭の3byteを使ってテキストの種類を書き込みます。

通常あっても基本問題ありませんが、現在の主流はテキストの内容を見て自動判断する方式です。windows10以降のデフォルト保存形式もUTF-8BOMなしです。

そしてwebで扱うHTMLもBOMは不要です。
普通のテキスト同じく基本的には問題ないものの、頭に3byteがクローラーなどの邪魔になる可能性がありBOMは消す必要があります。

■UTF-8で書き込み②(BOMなし)

With CreateObject("ADODB.Stream")
.Type = 2:.Charset = "UTF-8":.Open:.WriteText retext:.Position = 0:.Type = 1:.Position=3:tb=.Read:.Close
.Type = 1:.Open:.Write tb:.SaveToFile htmlpath, 2:.Close
End With

  1. CreateObject(ストリームオブジェクト)
  2. Type(2:テキストモード)
  3. Charset(文字コード:UTF-8)
  4. Openでストリームを開きます
  5. WriteText でストリームに書き込む
  6. テキストの状態でストリームの位置を0に
  7. Type(1:バイナリモードに変更)
  8. バイナリモードで開始位置を3byte移動
  9. Readで変数に4byte以降代入
  10. Closeでストリームをリセット
  11. Type(1:バイナリモード)
  12. Openでストリームを開きます
  13. Writeでストリームを書き込む
  14. SaveToFile でストリームを全て出力(2は上書き)
  15. Closeでストリームを閉じます

公式ドキュメントを見る限りCloseで閉じた後に再度Openで開いても問題なさそうなので1つのストリームでBOMを消しています。
処理が複雑そうになるならストリームは読み込みと書き込みで分けてください。

Closeメソッド
https://learn.microsoft.com/ja-jp/office/client-developer/access/desktop-database-reference/close-method-ado

処理としては文字列情報をテキストストリームに変換
UTF-8でテキストストリームに書き込むとBOMが入る
テキストストリームの状態でポジションを0で先頭に戻す
バイナリモードにして先頭からBOM分の3byteをスキップさせ変数にbyteで代入
ストリームを切断後に開きなおし、ストリームにbyteを代入あとはSaveToFileで出力して終了。

■UTF-8で書き込み③(BOMなし)

With CreateObject("ADODB.Stream")
.type=2:.charset="UTF-8":.open:.writetext retext
.position=0:.type=1:.Position=3:tb=.Read
.position=0:.seteos:.write tb:.SaveToFile htmlpath, 2:.Close
End With

もう少し詰めるならこう。
BOM3byteを飛ばすところまでは同じ。
ストリームの位置を先頭に戻し、seteosでストリーム内容を完全消去します。
そこにストリームを書き込みなおして出力。

コードの評価をジェミニなどで通したときにBOMが必ずつくか分からないと言われて調べたけれど、BOMが付かない状況を探しても見つかりませんでした。

WriteText メソッド (ADO)
https://learn.microsoft.com/ja-jp/office/client-developer/access/desktop-database-reference/writetext-method-ado

Charset プロパティ (ADO)
https://learn.microsoft.com/ja-jp/office/client-developer/access/desktop-database-reference/charset-property-ado

そもそもBOMについての記載がなく、仕様説明ないまま使われてる技術怖い・・・。

2025年8月15日金曜日

WindowsPowerShellのGUI版を作るメモ

 さて、今回はhtaとvbsを使ったローカルスクリプトをちょっとGUIのパワーシェルにして行こうかなって話。

パワーシェルでハローワールド

まずパワーシェルでハロワしましょう。
今回はファイルを作りたいのでパワーシェルに直打ちではないです。
文字を表示するだけならWrite-Hostを使います。
「Write-Host "ハローワールド";pause」とメモ帳に記載して「はろわ.ps1」UTF-8BOM付きで保存。


とりまOSデフォのパワーシェル5.1を使います。
デフォルトのエンコードがsjisなのでBOM付き保存が必要です。


※6以降を使う場合は別途インストールが必要。新しい方を使う場合は起動のexeが変わります。デフォの場合「powershell~」新しい方の場合は「pwsh~」になります。
※pwshの方のファイルはデフォがUTF-8BOMなしです。気になる場合はパワーシェルのコンソールで「[System.Text.Encoding]::Default」を打てば分かります。


セキュリティが高くてデフォルトでは実行出来ないので右クリックショートカット作成で「powershell -ExecutionPolicy Bypass -File C:\t\はろわ.ps1」パスは保存先のパスを指定。「-ExecutionPolicy」が実行ポリシーを現セッションのみ変更する指定で「Bypass」が全ての許可指定。「-File」はファイルから実行って意味。


GUIウィンドウでハローワールド

HTAの代わりにウィンドウを作らないといけないので次はウィンドウ表示

  Add-Type -AssemblyName System.Windows.Forms
  $form=New-Object System.Windows.Forms.Form
  $form.ShowDialog()

これだけ。
1行目は「.NET Framework」ライブラリのウィンドウ(フォーム)を使える様にする指定
2行目はオブジェクトを変数に代入
3行目で表示
ファイルを上書きして実行(実行方法はさっきと同じ)
そうするとパワーシェルウィンドウとなんも表示されないウィンドウが表示されます。
これはパワーシェルが本体で、そこからウィンドウを表示してるから。
ウィンドウを✕閉じするとパワーシェルも一緒に閉じます。

次にウィンドウのタイトルとウィンドウ内でもハロワしてみましょう
オブジェクトを代入する時に「 -Property @{プロパティ設定}」でプロパティを設定しながら代入出来ます。

Add-Type -AssemblyName System.Windows.Forms
$form = New-Object System.Windows.Forms.Form -Property @{
    Text = "ハローワールド"
    Size = New-Object System.Drawing.Size(600, 400)
}
$label = New-Object System.Windows.Forms.Label -Property @{
    Text = "ハローワールド"
}
$form.Controls.Add($label)
$form.ShowDialog()

htaと違ってウィンドウ内には直接文字を打てません。
テキストを入力するにはテキスト入力エリア=ラベルを作ってそこに文字を指定します。
更にラベルを作ったら、それをウィンドウに追加「$form.Controls.Add($label)」しないとウィンドウ上にラベルが配置されません。変更したら上書き保存。

今回から起動ショートカットのコマンドに「-windowstyle hidden」を追加します。
具体的には「C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -windowstyle hidden -ExecutionPolicy Bypass -File C:\t\はろわ.ps1」
これで表示メインじゃない起動元のパワーシェルを非表示(=バックグラウンド)にできます。


これでGUIハローワールドも終了

ボタン等の配置方法

htaはhtmlと同様の仕組みなので簡単にinputboxやtextarea、ボタンなどを配置出来ていましたが今回は違います。
テキストを表示するだけでもラベルが必要なようにhtmlなどで言えば全てのコントロール(テキストボックスやボタン等)がz-indexのように個々がレイヤー別にあり、コントロールに追加した順番に(後追加ほど手前に)表示されます。
<input type="button" value="フォルダリスト取得" onClick="getfolderlist()" >
としてた場合は   

$btnFolderList = New-Object System.Windows.Forms.Button -Property @{
    Text = "フォルダリスト取得"
    Location = New-Object System.Drawing.Point(680, 10)
    Size = New-Object System.Drawing.Size(100, 25)
}

こんな感じで必ず座標と基本的にはサイズの指定も必要になる
htaで「onClick="getfolderlist()"」と関数へキックしていたが、処理する関数を呼ぶのと違って自身のコントロール(変数)のクリックに処理を記述します
$btnFolderList.Add_Click({処理})
で記述する事が出来ます。ただ毎回「New-Object System.Windows.Forms.Button -Property @{~」と書くのは面倒なのでコントロールの操作を関数化しましょうか

function newbutton{
    param($tx,$x,$y,$w,$h)    
    $setbt = New-Object System.Windows.Forms.Button -Property @{
        Text = $tx
        Location = New-Object System.Drawing.Point($x, $y)
        Size = New-Object System.Drawing.Size($w, $h)
    }
    return $setbt
}

例えば、この様に関数を作ります
$btnFolderList =newbutton "フォルダリスト取得" 680 10 100 25
そうすれば、この様に関数で設定出来ます。
関数を呼ぶ時は「,」不要です。コンソールのコマンド系に倣ってこういう形です。
一応プログラム的な書き方にも対応していて
$btnFolderList =newbutton( "フォルダリスト取得",680, 10, 100, 25)
という記述方法も可能です。括弧を付ける場合、区切りは「,」に変更する必要があります。

テキスト入力コントロールの生成

次はテキスト入力エリア、htaでは以下通りですが
<input type="text" name="ifo" size="100" value="C:\">
<textarea name="txa" cols="120"  rows="25">リスト編集エリア</textarea>

パワーシェル(System.Windows.Forms)ではテキストエリアとインプットテキストとような区分けはありません。
テキストを入力するエリアに行入力(Multiline)を許すかどうかが違うだけで機能的にはTextBoxの挙動の差を設定するだけです。

function newtextbox{
    param($tx,$x,$y,$w,$h,$ml = $false , $sb = "none",$ww=$true)    
    $settb= New-Object System.Windows.Forms.TextBox -Property @{
        Text = $tx
        Location = New-Object System.Drawing.Point($x, $y)
        Size = New-Object System.Drawing.Size($w, $h)
        Multiline = $ml
        ScrollBars = $sb #Vertical/Both
        WordWrap = $ww
    }
    return $settb
}

こんな感じですね。
$txtReadPath =newtextbox $desktoppath 70 10 600 20
$txtArea=newtextbox "リスト編集エリア" 10 70 760 400 $true "Both" $false
呼ぶ時はこの様に記述し、デフォルト値を設定すれば引数のありなしも制御可能です。

HTAツールをパワーシェルGUIツール化

さて、概ねパワーシェルのGUI化が把握できたので実際に過去に作ったHTAツールをパワーシェルでGUI化してみます

Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName Microsoft.VisualBasic

# 定義
$desktoppath=[Environment]::GetFolderPath("Desktop")

# フォームの作成
$form = New-Object System.Windows.Forms.Form -Property @{
    Text = "一括フォルダ生成"
    Size = New-Object System.Drawing.Size(800, 600)
    StartPosition = "CenterScreen"
}

# コントロール生成関数
function newlabel{
    param($tx,$x,$y,$w,$h)    
    $setlabel = New-Object System.Windows.Forms.Label -Property @{
        Text = $tx
        Location = New-Object System.Drawing.Point($x, $y)
        Size = New-Object System.Drawing.Size($w, $h)
    }
    return $setlabel
}
function newtextbox{
    param($tx,$x,$y,$w,$h,$ml = $false , $sb = "none",$ww=$true)    
    $settb= New-Object System.Windows.Forms.TextBox -Property @{
        Text = $tx
        Location = New-Object System.Drawing.Point($x, $y)
        Size = New-Object System.Drawing.Size($w, $h)
        Multiline = $ml
        ScrollBars = $sb #Vertical/Both
        WordWrap = $ww
    }
    return $settb
}
function newbutton{
    param($tx,$x,$y,$w,$h)    
    $setbt = New-Object System.Windows.Forms.Button -Property @{
        Text = $tx
        Location = New-Object System.Drawing.Point($x, $y)
        Size = New-Object System.Drawing.Size($w, $h)
    }
    return $setbt
}

# コントロールの作成
# tx
$lblReadPath = newlabel "読込パス:" 10 10 60 20
$lblOutputPath = newlabel "出力パス:" 10 480 60 20
# txbx
$txtReadPath =newtextbox $desktoppath 70 10 600 20
$txtArea  =newtextbox "リスト編集エリア" 10 70 760 400 $true "Both" $false
$txtOutputPath =newtextbox $desktoppath 70 480 600 20
# btn
$btnFolderList =newbutton "フォルダリスト取得" 680 10 100 25
$btnFileList =newbutton "ファイルリスト取得" 680 40 100 25
$btnMakeFolders =newbutton "フォルダ生成" 680 480 100 25
$btnNumberSequence =newbutton "連番入力" 10 510 100 25

#ボタン処理
$btnFolderList.Add_Click({
    if ($txtReadPath.Text -eq "") {
        $txtArea.Text = "ディレクトリを入力して!"
        return
    }
    if (-not (Test-Path $txtReadPath.Text -PathType Container)) {
        $txtArea.Text = "ディレクトリが正しくありません"
        return
    }
    $folders = Get-ChildItem -Path $txtReadPath.Text -Directory | ForEach-Object { $_.Name }
    $txtArea.Text = $folders -join "`r`n"
})
$btnFileList.Add_Click({
    if ($txtReadPath.Text -eq "") {
        $txtArea.Text = "ディレクトリを入力して!"
        return
    }
    if (-not (Test-Path $txtReadPath.Text -PathType Container)) {
        $txtArea.Text = "ディレクトリが正しくありません"
        return
    }
    $files = Get-ChildItem -Path $txtReadPath.Text -File | ForEach-Object { $_.Name }
    $txtArea.Text = $files -join "`r`n"
})

$btnMakeFolders.Add_Click({
    if (-not (Test-Path $txtOutputPath.Text -PathType Container)) {
        [System.Windows.Forms.MessageBox]::Show("出力先ディレクトリが正しくありません", "エラー", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error)
        return
    }
    $outputPath = $txtOutputPath.Text
    if (-not $outputPath.EndsWith("\")) { $outputPath += "\" }
    $folderNames = $txtArea.Text -split "`r`n" | Where-Object { $_ -ne "" }
    foreach ($folder in $folderNames) {
        try {
            New-Item -Path ($outputPath + $folder) -ItemType Directory -ErrorAction Stop | Out-Null
        } catch {
            # エラーは無視(既存フォルダなど)
        }
    }
})

$btnNumberSequence.Add_Click({
    $myput = [Microsoft.VisualBasic.Interaction]::InputBox("テキストエリアに数字連番生成するだけ`n数字を入力してください。", "連番入力")
    if ($myput -eq ""){return}
    if (-not ($myput -match '^\d+$')) {
        [System.Windows.Forms.MessageBox]::Show("数値(整数)を入力してください", "エラー", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error)
        return
    }
    $n = [int]$myput
    $len = $myput.Length
    $result = 1..$n | ForEach-Object { $_.ToString("D$len") }
    $txtArea.Text = $result -join "`r`n"
})

# コントロールをフォームに追加
$form.Controls.AddRange(@($lblReadPath, $txtReadPath, $btnFolderList, $btnFileList, $txtArea, $lblOutputPath, $txtOutputPath, $btnMakeFolders, $btnNumberSequence))

# フォームの表示
$form.ShowDialog()

1つずつ処理確認しつつほぼそのまま実装出来ました

2025年8月7日木曜日

Astria Ascending(アストリア アセンディング)をやり込んだ感想

Astria Ascending(以降AA)個人的かつ 最終的な感想としては「面白かった」です。
ただ、おそらく自分がやりこみの深いコアゲーマーであるので一般的ユーザーと感想が一致しないような気はします。特にライトユーザーとかにはゲームとして難しいかもしれない。
まぁ、ゲーマーの定義についはまた今度考えるとしましょうか。

バグが多いし不親切

とは言え、「2025年(ver1.07)時点バグ」で纏めた通りまぁまぁバグが残っていますし、とにかくこのゲームシステムの説明とかが不足してるし、クエストなど不親切な所が多いので不満が並ぶのも分かります。

ただ、余り古いサイトの情報ばかりみてあげて欲しい。
古い記事をみるとトロコン出来なかったとかあるので昔の状態ではそうとうヤバそうではありますが少なからず、現行のバグは基本的に進行不能とかはなく、普通にプレイする分には大丈夫です。
感想を検索すると古い情報が多いので進行不能にはならないはず・・・とだけ。

ライトユーザーは強制されなければ理解しようとしない

さて、LOMでも思ったけれど強制的じゃなければゲームシステムなんてたぶん理解しようと思わない。かなりチュートリアルを念入りにやらないと無理だし、なんならチュートリアルを入れても強制的(それを理解しなければ先に進めない)じゃなければ理解しない。
LOMはやりたい人だけやればいいシステム部分だけど、AAのフォーカスシステムは理解しなくても一先ずなんとかなるせいで難易度がノーマルでもかなり難易度が高めに感じる。
二周目ではシステムの理解が深まった事でノーマルだとほぼ楽に進める様になったが、そもそもライトユーザー・・・というか一般ユーザーはゲームを二周しない。

ジョブについて説明不足

このゲームの良くない所としてはまず第一にジョブのやり直しがきかないのに全然説明がない所。普通に進めるとどのジョブがどんなアビリティを使えるかすら分からない。

これを補足するとゲーム上全く説明がないのだが、「日誌」>「ジョブ」からジョブの覚えるアビリティなどを事前に確認出来る。不親切すぎる!!教えてよそれ!!!気づいたのだいぶ後だよ!!


色々あったがゲーム自体は面白い

さて、色々悪い所を先に記述したがトータルでは面白かったのでその部分について

アクションパート、謎解きパート、ショートカット

この手のアクションゲームパートはいかにシステムを悪よ・・・げふん。
いかにシステムを上手く使うかが攻略として面白い。
例えば「みずがめ座神殿」などはどこのサイトも滑空を取得しないと攻略出来ないと書いているが、彫像転移でもクリア出来る。

こういった攻略を考えるのも面白い。
更に滑空延長を使えば本来強制される「やぎ座神殿」の4つか5つほど攻略フロアをスキップ出来る

滑空延長や多段ジャンプ等は「特殊操作テクニック」にまとめてあるのでそちらを見てもらうとしてこういった事が出来るのも面白かった。
特殊操作テクニックを使わなくてもクリア出来るのだが、アクションゲームが苦手な方やパズルが苦手な人には感想を見るに一部では不評。ゲームを作るのって難しいね。
ヴァルキリープロファイルとかが面白い人にはおそらくアクションパートも面白いと思う。

戦闘システム、フォーカスシステム、属性システム

二周目ではかなり上手く使えて面白かった。
弱点を突いてフォーカスポイント(以降:FP)を貯めてそれを使って大ダメージを与える。
属性が重要なのでヒューズやウィークネスが非常に役に立つ。

戦闘の根幹をなすシステムなのだが、色々な感想をみると戦闘メンバーが(主にダグマに)固定されるみたいな事が書いてあって、シャーマンやハンターの弱点付与や味方の属性変更をほぼ理解してない故の問題に感じました。
先に述べた通り、ジョブの説明不足が招いてる問題に感じる。
シャーマンもハンターも・・・というかどのジョブも必ず二人だれかが取得できるので固定しなくても戦えるシステムになっている。

ただまぁこれも一周回ってケイディンの【ハンターのアビリティ「ウィークネス(敵の弱点上書き)」+アサシンのサポートアビリティ「波及(異常全体化)」】が便利でアルパジョでウィークネスとってもなぁ・・・という一歩先を行ったメンバー固定になる可能性はある。

「ウィークネス+波及」を紹介してる所もまぁまぁあるが、ウィークネスは大半のボスには効かないのでほぼ雑魚戦用であり、やり込んだ結果実は雑魚戦用ならもっと良いアビリティが存在する。それは組み合わせなのでジョブの方で紹介します。

ジョブシステム、アセンションツリー

「ウィークネス+波及」の様にジョブを上手く選択すればかなり戦闘が有利になる
ジョブの組み方によってかなり色々な事が出来て面白かった。

ただ・・・頭に述べている通り戦闘システムと関連して根幹をなすシステムなのに説明不足すぎる。
大半のプレーヤーは手探り過ぎて上手くアビリティを使えていない。
先ほど述べた通り、シャーマンとハンターが上手く使えてないライトユーザーが殆ど。
更に言えば、アビリティ説明も結構嘘が多いので困る。

例えば時間術師のリブートの説明は「ターゲットを戦闘開始時の状態に戻す」だがこれは全くの嘘で、正しくは「全てのバフを消し去りバフカウンタをリセット、HPMPを最大値に回復」する。

例えば戦闘開始時にHPが1でMP0であっても、リブートをするとHPとMPを全回復する。
つまり、戦闘開始時のキャラの状態なんて保存しておらず、単純に最大HPと最大MP分の回復をする。
更に言えば、ガーディアンの転移でHPとMPで入れ替えてる状態でリブートすると元々HPとMPになる。この処理の見る限り本当に単純に通常時の最大HPと最大MPを現在のHP欄とMP欄に代入してるだけなのです。
サイトの方では書いてませんがバフカウンタリセットがこれまた特殊な利便性をはらんでいるのですがまぁその利便性はゲームをつまらなくするので省略。

まぁとにかく、リブートが超便利な神アビリティなわけです。

次にガーディアンの「累積」です。これは3ターンダメージを保留し4ターン目開始時にまとめてダメージを受けるアビリティです。
これだけ見るとそうなんだー程度なのですが、この累積は上書きできます
これを理解してるかどうかで価値が全く違います。
つまり、「1:累積発動ターン」「2:自由行動ターン」「3:再累積ターン」と繰り返す事で累積を行うキャラクターは一切のダメージを受けません。
戦闘終了まで累積をした場合、ダメージの借金を踏み倒します
ただしジョブの所で説明している通りターン処理は自分にターンが来た時に消費されます。
再行動系のサポートアビリティを付けてるとターン調整に失敗します。

更にこの累積は即死を無効化します
どういうことかというと即死条件スキルのデメリットを無効化します
1つ目は鉄壁です。本来は味方の全ダメージを無効化して自身がそのダメージを受けて受けきっても即死するという使い勝手悪いアビリティなのですが累積があると話が変わってきます。
自身もダメージを受けないし即死も無効。その上味方のダメージは0になる。最強防御になります。
2つ目は戦士の犠牲。自身の死亡を無効化しつつ、味方一人復活。
3つ目は戦士の殉職。自身の死亡を無効化しつつ、味方全員復活。
4つ目は黒騎士のディスパース。
自身を指定すれば死亡する事なく味方全員のHPMPを回復できます
5つ目は黒騎士の消耗。自身の即死を無効にしつつ、敵を100%即死

そうです。サイト側で神アビリティ認定していますが黒騎士の「消耗」は自身を即死させて即死免疫のない敵を必ず即死させます
即死アビリティはどれも確率(アサシンの暗殺や時間術師の死後の世界など)なのにこの消耗だけは確率の記載がない通り必ず敵を即死させます。
つまり、クレスで累積を行い消耗を使えばノーリスクで敵を100%即死出来るという事です。


続けて紹介するのはハーネッサーのオムニキャストこのアビリティは対象を指定するアビリティの対象を全体化します(ランダム対象や自身固定アビリティは不可)

指定さえできるアビリティなら全体化するというのが超絶便利効果で、少し前に「ウィークネス+波及」よりいい方法があると記載しましたが、それは「オムニキャスト+消耗」です。
効果は全ての敵を100%即死です。超ぶっ壊れアビリティの完成です。

このゲームにおける全ての通常戦闘、バトルチャレンジも含めボス以外なら全ての敵を1ターンで即死可能です。属性も何も関係ありません。

バトルチャレンジはAGIさえちゃんと確保できればレベルが低くてもボスを除き各チャレンジが1・2分で終わります。

属性の強弱やFPで大ダメージの概念はどうした・・・はさておき。
とまぁ、こんな感じにシステムを理解するほど色々やり込みが出来るわけです。


ミニゲーム:J-STER、シューティング

J-STERはまぁまぁ楽しめました。
トークン値の暴力すぎるという意見もあるけどそれはFF8やFF9のカードゲームもそうなのでとくには気になりませんでした。
基本的にトークン値前後±1か2くらいだとよく考えて返す必要があり面白かったです。
トークンコンバーターがFF8のカード変化とかの位置なのですがXPもSPも変換量が微妙で不要カード減らしてランダム戦を楽にする以外の使い道がないのが勿体ない。

あと、致命的ではないもののカードの強さでフィルタして交換するとフィルタのタブに居るのに内容だけが全体リストに戻されるという超面倒臭いバグが残ったままです。
他にもカードゲームする時に出すカードを選択する画面で位置をある程度移動した状態でカードの強さでフィルタしてタブを動かすと選択してるトークンと画面左の選択画面の位置情報がずれるという絶対に気付くだろうバグも残ったままです。

トークンの選択周りはバグが多すぎる。
放置されたまま直りませんし、たぶん致命的な問題にならないので今後も直らない気がします。

ランダム戦で全体からランダムなのも地獄な所で、もう少し調整するべきだったんじゃないかなぁ・・・。敵も弱いカードをランダムで選ぶならまだしも敵だけ強く不満が溜まるし、トークンを減らしたくてもトークンコンバーターが得られる所までチャプターが進まないといけない。
さらに追い打ちで先ほど述べたバグ、トークンの強さでフィルターを掛けて交換しても交換するたびに全体リストになって一番上に戻るバグによって交換もかなり時間が掛かってしまう。
トークン選択周りはデバッグしてないとしか思えない。

シューティングゲーム

ミニゲームとしてダメとは言わないのですが、本編で強制するならもう少し緩めで良かったかもしれない。
キャラの当たり判定がデカすぎ。敵の攻撃当たり判定も広い。
属性を使いたかったのも分かりますが、斑鳩でも2属性なのに8種類も属性あったら切り替えが間に合いません。

これに至ってはプレイすると移動してしまって連続プレイも面倒。

2025年5月5日月曜日

JSON.parse(JSON.stringify())で深いコピーをしないでという話

javascriptで深いコピーをする方法について

前回は浅いコピーの話をしたので今回は深いコピーの話

■JSON.parse(JSON.stringify(配列))

let originalarr= Array(36).fill().map(() => Array(8).fill(0));
let copiedarr= JSON.parse(JSON.stringify(originalarr));
copiedarr[0][0] = 1; console.log(originalarr[0][0]); // 出力: 0 (影響を受けない)

 この方法は有名で簡潔に深いコピーを実現できます
ただし、JSON.parse(JSON.stringify(配列)) は、手軽に深いコピーを実現できる反面、undefined、関数、Date オブジェクト、RegExp オブジェクト、Map、Set、循環参照などの特定のデータ型を正しく扱えないという大きな制約があります。


この方法でコピーするのがおすすめのパターン

以下のような比較的単純な構造のオブジェクトや配列を深いコピーする場合には手軽で有効です。
・プリミティブな値 (number, string, boolean, null) のみを含む配列やオブジェクト
・プレーンな JavaScript オブジェクト (POJO - Plain Old JavaScript Object) で、上記のプリミティブな値と基本的なオブジェクト、配列のみで構成されている場合


そもそもこれは何をやっているのか

使い方と使う場面を知ったら次は仕組みの理解。

■そもそもJSONってなに?
JSON 「JavaScript Object Notation」の略です。
JavaScript のオブジェクトを軽量なテキスト形式で表現するための標準的なデータ形式です。その名前が示す通り、JavaScript のオブジェクトの記法をベースにしていますが、言語に依存しないため、様々なプログラミング言語やシステム間でデータを交換する際に広く利用されています。


JSON.stringify() メソッド

まずは内部から理解しましょう。
構文は以下パターン。
JSON.stringify(value)
JSON.stringify(value, replacer)
JSON.stringify(value, replacer, space)

「JSON 文字列に変換する」というのが根本的な機能です。
例外のTypeErrorが発生するのは「value が循環参照を含む場合・長整数値に遭遇した場合」です。
上記に記載している通り色々な言語間のデータをやり取りなどが可能な文字列形式にするという事です。ですので、JSON.stringify(配列)を行うとstringを出力します。 


JSON.parse() 静的メソッド

JSON 文字列を JavaScript のオブジェクトや配列に変換します。
つまり、逆の事を行います。JSON 文字列を解析して、元の配列やオブジェクトにします。


■つまり?

つまり、「JSON.parse(JSON.stringify(配列))」というのは配列を一旦文字列にしてそれを再度配列に戻すという事を行っています。
一旦文字列になる事で配列ではなくなるので新たに再変換されて出力される時に新しい配列になるので多次元配列がディープコピーできるのです。
また、この処理を行う時に文字列に上手く変換できない値達は欠損するという事です。


■素直に配列でコピーする

関数化する

function deepCopyMultiDimensionalArray(arr) {
if (!Array.isArray(arr)) {
return arr; // 配列でなければそのまま返す (プリミティブな値やオブジェクトなど)
} const newArray = [];
for (let i = 0; i < arr.length; i++) {
newArray[i] = deepCopyMultiDimensionalArray(arr[i]); // 要素が配列なら再帰的にコピー
}
return newArray;
} //ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー // 使用例 let originalMulti = [1, [2, [3, 4]], 5, { a: 6 }];
let copiedMulti = deepCopyMultiDimensionalArray(originalMulti); copiedMulti[1][1][0] = 99;
copiedMulti[3].a = 100; console.log("オリジナル:", originalMulti);
// 出力: オリジナル: [ 1, [ 2, [ 3, 4 ] ], 5, { a: 6 } ]
console.log("コピー:", copiedMulti);
// 出力: コピー: [ 1, [ 2, [ 99, 4 ] ], 5, { a: 100 } ] let originalWithObject = [1, [2, { b: 7 }]];
let copiedWithObject = deepCopyMultiDimensionalArray(originalWithObject); copiedWithObject[1][1].b = 101;
console.log("オリジナル (オブジェクトあり):", originalWithObject);
// 出力: オリジナル (オブジェクトあり): [ 1, [ 2, { b: 7 } ] ]
console.log("コピー (オブジェクトあり):", copiedWithObject);
// 出力: コピー (オブジェクトあり): [ 1, [ 2, { b: 101 } ] ]

こんな感じ


■二次元配列のコピーでいい場合

まぁでも多次元配列なんてあんまり使う物でもないと思いますので(多次元で深くネストすると人間も何処に何があるか分かり難くなるので)、大半は二次元配列とかが多い(セルや座標)と思います。

その程度であれば1次配列を[...配列]で展開して回すだけでも十分です。
function deepcopy2d(oa) {return oa.map(ar=>[...ar]);}


■structuredClone(obj)

実はもっと簡単な方法があります。

let original = {
  date: new Date(),
  regex: /abc/g,
  map: new Map([['key', 'value']]),
  set: new Set([1, 2, 3]),
  arrayBuffer: new ArrayBuffer(8),
  circular: {}
}; original.circular.self = original;
let cloned = structuredClone(original);
console.log(cloned.date); // 元の Date オブジェクトとは異なる新しい Date オブジェクト console.log(cloned.regex); // 元の RegExp オブジェクトとは異なる新しい RegExp オブジェクト
console.log(cloned.map);  // 元の Map オブジェクトとは異なる新しい Map オブジェクト
console.log(cloned.set);  // 元の Set オブジェクトとは異なる新しい Set オブジェクト
console.log(cloned.arrayBuffer); // 元の ArrayBuffer とは異なる新しい ArrayBuffer
console.log(cloned.circular.self === cloned); // true (循環参照も正しくコピー)
cloned.date.setTime(0);
console.log(original.date.getTime() === cloned.date.getTime()); // false (深いコピー)

こんな感じです。
見ての通り、より多くのデータ型をサポート: Date オブジェクト、RegExp オブジェクト、Map、Set、ArrayBuffer、DataView、ImageBitmap、ImageData など、JSON.stringify() では正しく扱えなかった多くの型を適切にコピーできます。

循環参照を処理: structuredClone() は循環参照のあるオブジェクトグラフも正しくコピーできます。内部的にコピー済みのオブジェクトを追跡し、無限ループを防ぎます。

Transferable オブジェクトの転送: ArrayBuffer などの Transferable オブジェクトは、コピーではなく所有権が移動するため、より効率的なデータの受け渡しが可能です(元のオブジェクトでは使用できなくなります)。

より自然なコピー: JSON.stringify() のように一旦文字列にシリアライズするステップがないため、より直接的で自然なコピー処理が行われます。


しかもwindow.のグローバル関数なのでこれでオールOK!
実装されたのは2020年頃でnodeも2021年頃実装しています。

2025年4月28日月曜日

ランドメイク&全イベント統合シミュレーターを作ろうかなって話

「 Hey!Scripting Cat!全イベントのシミュレーションもしたいよ!」

HAHAHA!なかなか無理をいいますね。この令和の時代に需要が何処にあるんですか。
ひとまずは「ランドメイクシミュレーター」は作ってあるからそれを使ってね
現状でもAFイベントはセットになってるからプレイ時に可能な配置をシミュレーションできるよ。
とは言え確かにあそこまでやるならもう全部のイベントの消滅条件とかまで管理しちゃえばいいんじゃない?という気もする。あと連続(アンドゥ・リドゥ)描画をした時にちょっと遅いのでそこを解消したいですね。

ランドメイク&全イベント管理を考える

まずは現在のランドメイクシミュレーターの機能と追加したい機能

■現在のランドメイクシミュレーターの機能
・全体マップを選択する
・ランドメイクマップを生成6x6(地形と土地マナ)
・ポスト入手→配置→積み木入手→配置
積み木まではイベントがない為配置と同時にAF取得
以下ループ
┏┓
┃┃
┃■イベント開始
┃┃
┃■イベント終了
┃┃
┃■AF入手
┃┃
┗┛
・常に工程を復元用ログに保存

■現在サブ機能
・全体マップの表示非表示
・全体マップ選択範囲クリック選択
・全体マップ選択範囲矢印で操作
・推奨エリアの直接選択
・配置とイベント両方のログ
・配置とイベント両方のアンドゥリドゥ

ーーーーーーーーーーー

■初期化
・Fマップ生成
・Lマップ生成
・リスナー設置
 ┗Fマップ 25x25
 ┗Lマップ 6x6
 ┗ボタン類 12個前後
 ┗AF 26 
 ┗イベント 約68x2
 ┗チェック 2程度

■操作
Fマップ選択>インデックス変更判定>変更時再初期化
繰り返し
 ┗イベント開始or終了をチェック
 ┗AFを選択
 ┗AFを設置
 ┗イベントの取得

■イベント
・開始または終了チェック時にAF取得判定
・終了チェックでランドロック解除
・履歴加算

■AF設置時
・AF選択時 他の操作を停止、AF情報表示、配置可能エリア光らせる
・(キャンセル時:各操作停止解除、AF情報を閉じる、配置エリアを戻す)
・AF配置 AFがランドになる際に周囲4つのマナ合計÷2を加算して配置、周囲4つのランドはAFのマナを直加算
・配置終了時(各操作停止解除、AF情報を閉じる、配置エリアを戻す)
・設置終了時イベントを解放
・設置終了時履歴加算
 
■履歴操作
・各操作でイベント操作を記憶
・アンドゥ、肯定を1つ戻す
・リドゥ、履歴範囲内で履歴を一つ進める
・履歴操作中に履歴加算がある場合現在の位置を履歴の末端に変更

■状態の保存復元
SaveURLに現在の状態をパラメータで保存
URLにアクセスするだけで復元可能

鶏が先か卵が先か

・イベントが終了するとAF取得(ない時もある)
・AFを設置してランド生成でイベント生成

ランドが無いとイベントがそもそも発生しないのでAFが先
ポストを配置→ホーム生成→ナンバリング0イベント草人→AF入手

2025年4月27日日曜日

onclickを廃止した話

「Hey!Scripting Cat!モジュール読み込みにしたらボタンのonclickで関数が実行できなった!どうすればいい?」

なるほど良くある話ですね。HTML の onclick 属性は、イベント発生時にグローバルスコープで評価されるため、モジュールスコープ内で定義された関数を直接参照しようとすると、その関数が見つからず実行できません。

じゃあonclickが使えなくなるのかと言えばそうではありません。

モジュール読み込みでボタンから関数を実行する方法

web上のjavascriptにはグローバル領域に直接スクリプトを読み込む方法とモジュール(module)で読み込む方法の主に二種類があります。
今回の目標はボタンを押して関数を実行する事が目的です。
工程は2つあります「ボタンを押す」「関数実行」。これを実現する方法は2つ存在します。


1.window.領域に関数を持ってくる(非推奨)

今までの感覚を変更しないのであれば一番簡単です。
変更するのは関数側です。

■js
function moduleFunction() {console.log('関数実行');}
window.moduleFunction = moduleFunction;

これだけです。htmlも変更不要で簡単ですね。
現在onclickが動かないというのはつまりグローバル領域に関数が居ない為です。
じゃあ何処にいるのかというとモジュールスコープ領域に居ます。

つまり、window.関数名に現在の関数名を代入すればモジュール内の関数をhtml側からonclickで実行できます。折角モジュール読み込みするのであれば(スコープを気にし始めたのなら)一応非推奨ではありますが他の関数等はモジュールにいるので今までの通りの方法「<script src="script.js"></script>」でただjsを読み込むだけより全然良いと思います。
onclickが出来ないならレガシーな通常読み込みに戻そうと考えてしまうより一旦はこの方法で良いと思います。モダンへの入り口は広い方がいい。


モジュールに移行する理由

個人や小規模なサイトであれば必ずしもモジュール読み込みの形式にする必要はありません。ただ、そもそもモジュールで読み込む方法が生まれたという事は理由が、つまり利点があるから実装されました。

最大の理由はおそらく名前の衝突です。グローバルスコープに全ての変数や関数があると衝突して正しく動かなくなる可能性高くなります。

<script type="module" ></script>で直接モジュール領域を作ったり
<script type="module" src="script.js"></script>モジュール読み込みを行うとグローバルスコープを侵さずに衝突しない領域を作ることができます。

次に個々に領域を分断した事でそれをつなぐインポートとエクスポートが便利だからです。
これも結局はスコープの制御がしやすいという話に帰結します。


2.addEventListenerでボタン待ちをする

さて、次はよりモダンな方法です。
まずはボタンにidを付けます(他にも方法はある後述)。
■html
<button id="mybutton">ボタン</button>
■js
function moduleFunction() {console.log('関数実行');}
document.getElementById("mybutton").addEventListener('click', moduleFunction)

これだけです。それほど難しくはなりません。


モジュールのトップレベルのコードは、モジュールが最初に読み込まれ、評価される際に一度だけ実行されます。このタイミングで addEventListener を呼び出すことで、ボタン要素にクリックイベントのリスナーが登録され、ボタンがクリックされるのを待機する状態になります。

これによってボタンが押されるとidで対象のボタンが特定されクリックが実行されたことでmoduleFunctionを実行できます。
少し注意点としては「addEventListener('click', moduleFunction)」の関数名部分(この場合「moduleFunction」)には「()」をつけてはいけません。
関数登録に括弧が付くとその位置で即時関数が実行されてしまいます。

※ボタンを特定する他の方法
id 以外に、nameやclassやタグ名で要素を取得できます。
・class(例: document.querySelector('.mybutton'))
・タグ(例: document.querySelectorAll('button'))


引数を渡したい場合

括弧が無ければじゃあ引数渡せないのかというとそうではなく
document.getElementById("mybutton").addEventListener('click', ()=>{moduleFunction(1)})
とすれば引数を渡す事が出来ます。
これでonclickとはさよならできますね。

関数に飛ばさず直接処理する場合

document.getElementById("mybutton").addEventListener('click', ()=>{ここに処理を記述する})

こうするだけでOK。
さらっと流してるアロー関数部分は前の記事を参照してください。(https://blackstraycatreboot.blogspot.com/2025/04/blog-post.html)
要するに先ほどアロー関数に関数名を記述してるのは無名関数にする事で処理領域を確保して、この領域で改めて関数にキックしてたわけです。
ですから、ここに関数を記述せずに処理を記述すればOK!



余談

ちなみに「addEventListener」は実行時にイベントオブジェクトを渡します。
この場合、'click' イベントに対するリスナーなので、event は クリックイベント に関する情報を保持します。
今回のclick イベントの場合はeventの型はMouseEvent オブジェクト。
MouseEvent は Event クラスの派生型で、クリック固有のプロパティ(例: マウス座標)を含みます。


function moduleFunction(arg, event) {
  console.log(`引数: ${arg}`);
  // イベントオブジェクトの詳細を確認
  console.log(`クリックされた要素:`, event.target);
  console.log('イベントオブジェクト:', event);
  console.log('type:', event.type); // "click"
  console.log('target:', event.target); // <button id="mybutton">...
  console.log('clientX/Y:', event.clientX, event.clientY); // 例: 150, 200
  console.log('ctrlKey:', event.ctrlKey); // 例: false
}
document.getElementById('mybutton').addEventListener('click', (event) => {
  moduleFunction(1, event); // event オブジェクトを渡す
});

こういった情報取得も可能です。
ただまぁ今回は「ボタンを押す」「関数実行」が主題なので今回はこれで終了。