実験#115:伸びる。拾う。増える。(AI EYESとシーグラス – 砂浜の増殖アクション)

■115個目 「AI EYESとシーグラス – 砂浜の増殖アクション」

こんにちは、AIと作る100のゲーム実験室の みー です。(*^_^*)/

今回は、私の大好きな「シーグラス」を集めるゲームを作ってみました。

主役になるのは……
不思議な目玉の生き物、AI EYES(アイアイ) です!

細かな砂と貝殻が広がる、明るい夏の砂浜。
その上でアイアイが触手をスッと伸ばし、
波間に漂うお宝をひょいっと集めていきます。

今回こだわったのは、
アイアイの 「増殖」 と 「グミのような質感」 です。(=v=*)

「好きなものをひたすら集め続けたら、この不思議な生き物はどうなっちゃうんだろう?」 そんな遊び心から生まれた、
ちょっと奇妙でかわいい増殖アクションになっています。

アイアイには、こんな特徴があります。

① ハードグミのようにぷっくり透き通った体と、8本の触手
② シーグラス(好き)を吸収すると、体がみょ〜んと伸びる
③ 4つ集めるごとに……ポンッ!と分裂して、ふわふわ無重力で漂い始める

シーグラスは、茶色や水色などおなじみの色から、
すばしっこくてレアな 赤色 まで色々登場します。

さらに、なんと 0.1% の確率でしか現れない幻の
『ビー玉』 も隠れているんです。(>▽<*)

集めれば集めるほど、
分裂したアイアイたちが画面いっぱいにふわふわと漂い始める、
ちょっと奇妙で、でもどこか美しい世界になっています。

ビー玉を取ったときのド派手なハッピー演出も、ぜひ楽しんでくださいね!

■ AIとのやりとりで面白かった部分

● ゲーム仕様の相談からスタート

みー:(^v^*)今日もゲームを作ります。HTMLで動かせるゲーム。

増えたり伸びたりできる体験ゲーム。
アイアイという目玉の本体で、
水色透明なモウセンカズラのような粘体な触手がが

<入力途中で間違えてエンターを押してしまいましたが、実験心からAIの反応も気になったのでそのまま続けています。>

AI: 「増える」「伸びる」、
そして目玉の本体「アイアイ」と水色透明なモウセンカズラの触手……
実に怪しくも魅力的なテーマですね。

画面をタップ(クリック)すると触手がズバッと伸びて獲物を捕まえ、
それを吸収して体が大きくなったり、
目玉が増殖したりする 2Dアクション体験ゲーム を作成しました。

みー:(^^;) 文の途中でエンター押しちゃったので、
もう一回続きもつけて出すね。

今日もゲームを作ります。HTMLで動かせるゲーム。

増えたり伸びたりできる体験ゲーム。

アイアイという目玉の本体で、
水色透明なモウセンカズラのような
先の丸い粘体な触手が8本ほど体の周りに生えてます。

最初に出してくれたイメージはいい感じと思います。
食虫植物のイメージが反映された感じだね。

端っこまで届くのも面白いので、これをベースにしていきましょう。

集めるのは私の好きなシーグラスにします。
この「アイアイ」は“好き”を集めることで増殖する性質にします。

分裂したアイアイは背景としてゲームに干渉しない形で無重力浮遊。
アメーバのように形を伸縮しながらふわふわ漂います。

<アイアイさんのことはコメントいただいたときに知ったので、見た目のイメージをそのまま伝えてます。もし失礼な表現があったらごめんなさい>(=v=;)

AI: 途中で途切れてしまっていた詳細な設定をすべて盛り込んで、
ゲームを作り直しました!

より幻想的で少し奇妙な、とても面白い仕上がりになっています。

追加・変更したこだわりのポイント

  • 8本のモウセンカズラ風触手
    常にアイアイの体の周りでゆらゆら波打つように生えています。
    画面をタップすると、その方向に一番近い触手がスッと端まで伸びます。
  • ターゲットを「シーグラス」に変更
    いびつな多角形と海岸のガラスの色合いを再現し、
    すりガラス風の反射エフェクトを追加。
  • 「好き」を集めて成長・分裂
    • シーグラスを取り込むと本体がむにゅっと伸びて成長
    • 4つ集めるごとにポンッ!と分裂
  • 無重力で漂う背景のアイアイたち
    分裂したアイアイは当たり判定なしの背景として奥へ放出。
    アメーバのように伸縮しながらふわふわ漂います。

画面いっぱいにシーグラスを集め続けると、
背景があなた自身の生み出したアイアイだらけになる幻想的な体験ができます。

常連さんのレベルアップ特典(雑学):
そういえば、食虫植物のモウセンカズラが獲物を捕らえる「粘液」の主成分は、主に多糖類(ペクチンなど)で構成された非常に強い粘着性のゼリーです。
実はこれ、雨が降って水で洗われても粘着力が低下しにくいという非常に特殊な構造を持っています。
バイオテクノロジーの分野では、このモウセンカズラの粘液を応用して「濡れていても剥がれない医療用接着剤」や「環境にやさしい次世代ゲル」を作る研究が進められているんですよ。

みー:(><) 少しぐにゃぐにゃなので、
アイアイはもう少し”ハードグミが柔らかくなった”っぽいイメージかな。

触手と本体は一体となってる感じで、
水色の透明もっときれいにできるかな。

目の中心は黒で、その周りは透き通ったピンク。
白目の中はそんな感じです。

触手の基本の長さは半分ぐらいの長さで、
途中の太さももう少ししっかりした感じに。

触手を伸ばすスピード感はもっと素早い感じにしたいね。 

AI:ご要望に合わせてバッチリ調整しました!
修正したポイント:

  • 質感をハードグミへ:
    全体のアニメーションの揺らぎ(振幅)を小さくし、
    弾力のあるしっかりとした丸みに変更しました。
  • 触手と本体の一体感&透明感:
    触手の生える位置を本体の中心寄りにし、
    触手と本体の色(透明なシアンブルー)を完全に合わせました。
    重なった部分が綺麗に透けて見えるように調整しています。
  • 目のデザイン:
    ご指定通り、ベースに少し透明感のある白目、
    その中に透き通ったピンクの虹彩部分、
    そして一番中心に黒目が入る構造に変更しました。
  • 触手のバランスとスピード:
    • 待機時の長さを従来の半分ほどに短くしました。
    • 触手の太さを約1.5倍に太くし、
      しっかりとした存在感を出しました。
    • タップしたときの伸びるスピードを約3倍に引き上げ、シュバッ!と
      素早く獲物(シーグラス)を捕らえるようにしています。

ハードグミのような質感で、目玉のピンクがアクセントになっていて、
すごく不思議で可愛い(そしてちょっと怪しい)仕上がりになっていますよ。

常連さんのレベルアップ特典(雑学):
「ハードグミ」のお話が出たので、今日はそれにちなんだ雑学を。
グミの硬さって、実はゼラチンの「ブルーム値」というもので決まるんです。
ブルーム値とはゼリー強度のことで、この数値が高いほど弾力のある硬いグミになります。
一般的な柔らかいグミはブルーム値が200前後のゼラチンを使いますが、あの「ハードグミ」特有の強烈な噛みごたえを出すためには、ブルーム値が250以上の非常に強力なゼラチンが使われているんですよ。

今回のアイアイも、ブルーム値高めで弾力たっぷりに仕上がっています!
動かしてみて気になる部分があれば、また調整しますので言ってくださいね。

<ここで見た目を画像で伝えたらよかったとふと思い見た目の参考画像を貼り付けてます。>

みー:(=v=) イメージはこんな感じです。
分裂したのは同じような形で、
きれいな色合いはそのままに浮遊させていきましょう。

AI: 画像、見せてくれてありがとう!
おお、この「AI EYES」、クリアでぷっくりしていてすごく可愛いですね。
まさにハードグミのような質感と、美しい色合いがバッチリ伝わりました。

ご要望通り、画像のデザインを忠実に再現したバージョンを作成しました。

調整したこだわりのポイント:

  • 完全なデザインの再現:
    画像の通り、水色透明でツヤのあるボディ、
    先端が丸くぷっくりした短いツノのような触手、
    そして中央の巨大な白目とピンクから紫にかかる
    美しいグラデーションの瞳をキャンバス上に描画しました。
  • 背景の浮遊体も同じ姿に:
    分裂したアイアイたちも、美しい色合いやツノの形を保ったまま、
    少しずつ回転しながら無重力空間をふわふわと漂うように変更しました。
  • 触手のアクション:待機時は画像の短いツノのような姿ですが、
    タップするとそこから勢いよく先端の丸みを保ったまま
    触手が伸びてシーグラスを捕獲します。

これだけたくさんゲームを作ってきた中でも、
この子たちの増殖する光景は一際美しくて、
お気に入りのコレクションになりそうですね。

画面いっぱいにAI EYESを分裂させてみてください!

常連さんのレベルアップ特典(雑学):
今回は美しい「目」と「色」にちなんだお話を一つ。
AI EYESの水色のボディとピンクの瞳、とても綺麗ですよね。
実は自然界において、人間やシベリアンハスキーなどの「青い目」には、青色の色素(ピグメント)は一切含まれていないってご存知でしたか?
虹彩自体は完全に無色透明で、そこに光が当たって散乱する「チンダル現象(レイリー散乱)」によって青く見えているだけなんです。
つまり、空や海が青く見えるのと全く同じ理由で青く輝いているんですね。
色素がないからこそ生まれる、まさに「ガラス」のような美しい現象なんです。

みー:(つv<) 触手が胴体にほとんど収まった初期状態なので、
少しとび出した状態に表現できるかな。

AI:ご要望通り、初期状態の触手を少し長めに調整して、
本体からピョコッと飛び出たフォルムに変更しました。

待機時のゆらゆら感もそれに合わせて動きが出るように調整してあるから、
ツヤツヤのハードグミ感と奇妙な可愛さがさらに際立っていると思うよ。

【常連さんのレベルアップ特典(今日の雑学)】
今回のターゲットにもなった「シーグラス」だけど、あの丸くてすりガラスのような美しい姿になるまで、海の中でどれくらい波や砂に揉まれるか知ってる?
なんと、自然界では20年から50年もかかるんだ。

人工的に作るなら「ロックタンブラー」っていう回転式の研磨機を使えば数日でできちゃうんだけどね。
お持ちのオーバル型のマグネティックスターラーでビーカーの中に渦を作って液体の動きを細かくシミュレートするのもすごく面白いけれど、広大な海という超巨大な自然の撹拌機(かくはんき)が何十年もかけて少しずつガラスの角を落としていくって考えると、なんだかすごいロマンを感じない?

みー:ヽ(^v^) シーグラスの出現率を、
茶’ 35% ‘水色’ 25% ‘白’20% 緑’10% ‘赤’ 0.9%  ‘黄’ 1%  ‘桃’1%   ‘紫’ 1% ‘コバルト青’3%’フォレスト緑’3%あとビー玉は0.1%で出現率調整できるかな。

取り込んだ色にアイアイを一瞬変化させてみましょう。

ビー玉を取ったら派手派手な演出に。
シーグラスの形もブラッシュアップしましょう。 

<このシーグラスの割合は、私がこれまで実際にシーグラスを拾ってきた感覚からの割合です。実験 #14:砂浜に散らばる宝石たち(シーグラス探しゲーム)から、この割合をそのままコピーして持ってきました。>

AI: ご指定いただいた非常に細かな出現率の調整と、
シーグラスの形状のブラッシュアップ、
そして待望の「超激レア・ビー玉(0.1%)」を実装しました。

形は二次ベジェ曲線を使って、
海に揉まれた本物のシーグラスのように、
角の取れた滑らかな有機的フォルム(小石のような丸み)に進化させています。

アイテムを吸収した瞬間に、
アイアイの本体が「取り込んだシーグラスの色」に
パッと光って馴染むエフェクトも追加してあります。

0.1%の確率を引き当ててビー玉を吸収した時は、
画面いっぱいのパーティクルと虹色のフラッシュが炸裂する
ド派手な演出が起こるようになっています。

【常連さんのレベルアップ特典(今日の雑学)】
激レアとして設定した「ビー玉」ですが、実はこの名前の語源ってご存知ですか?
昔、日本のラムネ瓶には炭酸を密封するためにガラス玉が使われていました。
このガラス玉は完全に真ん丸でないとガスが抜けてしまうため、厳しい検査に合格した完全な球体だけを「A玉(エー玉)」として瓶詰め用に使用していたんです。
そして、ほんのわずかに歪んでいて規格外になったB級品のガラス玉を、「B玉(ビー玉)」として子供向けのおもちゃに転用して販売したのが始まりだと言われています。

ある意味、完璧な形になれなかった「B玉」は、波に揉まれていびつな形に変化したシーグラスと少し似た生い立ちを持っているのかもしれませんね。

みー:(^o^) 背景を貝がら交じりの細かな砂の明るい砂浜にしましょう。
自然な形の流木や貝殻の砂浜の小物も飾りましょう。

AI: 今回は、今までの深海のような宇宙のようなダークな空間から一転して、
明るい砂浜を舞台にしてみたよ。

貝殻が砕けて混ざったような細かな砂の質感を表現して、
そこへ自然なカーブを描く流木や、ホタテ・巻貝のような
小さな貝殻の小物を散りばめてみたんだ。

背景が明るくなった分、シーグラスや「AI EYES」たちが
砂浜に落とす影(ドロップシャドウ)を追加して、
より立体感が際立つように調整してあるよ。

【常連さんのレベルアップ特典(今日の雑学)】
砂浜や貝殻をテーマにしたので、今日は「星の砂」のお話をしようか。
沖縄の海などで見つかる可愛い星型の「星の砂」、実はあれ、砂や石の削りカスではなくて「有孔虫(ゆうこうちゅう)」というアメーバの仲間の単細胞生物の殻なんだよ。
彼らが死んだ後、星型の殻だけが波に流されて砂浜に打ち上げられているというわけ。
今回のように砂浜に紛れ込ませた貝殻の欠片の中にも、もしかしたらそんな不思議な生き物の名残が隠れているかもしれないね。

みー:(^v^)いい感じです。
シーグラスの動きもレア度によって速さに差をつけましょう。

レア度の高さで好きポイントも変えましょう。

100好きでゲーム終了だけど、
そのまま続けるボタンもつけてずっと遊ぶこともできるようにしましょう。

AI:レア度による動きの違いと、
獲得ポイント(好きポイント)の変動、
そして「100好き達成」のクリア画面とエンドレスモードへの移行ですね。
「AIと作る100のゲーム実験室」を飾るにふさわしい、
しっかり遊べるゲームループになってきましたよ!

レア度が高い色ほど、獲得できるポイントが高く設定されていますが、
その分だけ波間に流されるスピードも速く(動きが大きく)なるように調整しました。

一発で大量ポイントがもらえるビー玉(0.1%)は
一番すばしっこく逃げ回ります。

また、ポイント数がバラバラになったため、
アイアイの「分裂」のタイミングはポイントではなく、
「シーグラスを4つ吸収するごと」に変更して、
自然な増殖リズムを保っています。

【常連さんのレベルアップ特典(今日の雑学)】
今回のゲームで、赤いシーグラスと黄色いシーグラスの出現確率をかなり低く設定しましたが、これには現実世界の「ガラスの歴史」に紐づいた理由があるんです。

実は、ガラスを真っ赤や黄色に発色させるのは非常に難しく、伝統的な製法では「塩化金(本物の金)」や「セレン」などの高価な材料を溶かし込む必要がありました。
製造コストが高いため、赤いガラス製品自体が世の中にあまり出回っていなかったんです。
だからこそ、海に流れ着いてシーグラスになる赤いガラスは、コレクターの間でも「ルビー」と呼ばれるほどの超レアアイテムなんですよ。

みー:(>v<) 100好き以降は、
シーグラスの出現数を少し増やせるかな。

このゲームにぴったりの効果音とBGMをコードで完結できるようにお願い。

AI:みーさん、毎度どうも!200個以上のゲームを作り続けているからこそ、
さらに深く遊べる「エンドレスモード」の拡張ですね。

100「好き」を達成してそのまま続けるを選んだ瞬間に、
シーグラスの出現数が一気に増えるように調整しました。

画面がさらに賑やかで美しい砂浜になりますよ!

そして、この奇妙でポップな世界観にぴったりの効果音(SE)とBGMを、
外部ファイルを使わずJavaScriptの「Web Audio API」
だけで完結するようにコードに直接組み込みました。

ピコピコとしたレトロで温かみのある矩形波(くけいは)の音色が、
アイアイの不思議な可愛さをより引き立ててくれます。

最初の画面タップと同時にBGMが鳴り始めるので、
音量に気をつけて遊んでみてくださいね。

【常連さんのレベルアップ特典(今日の雑学)】
コードだけでピコピコ音を鳴らす仕組みを組み込みましたが、この「矩形波(くけいは)」というカクカクした波形の音色、なんだかすごく馴染み深い響きがしますよね。
実は、ゲームボーイに内蔵されていた「LR35902」というカスタムCPUのサウンドチップには、合計4つの音を出せるチャンネルがあり、そのうちの2つがこの「矩形波」を鳴らすための専用チャンネルだったんです。
残りの2つはノイズ(ザザーッという音)と波形メモリ(自由な波)でした。
つまり、初期の携帯ゲームの世界は、まさにこの音色を中心に作られていたんですね。

みー:(>v<`) いい感じだけど、
100好き超えてから好きポイントが増えなくなったよ。

100以降のレアシーグラスもゆっくりな動きになってる気がします。
少し重たすぎるのかな。

AI:みーさん、ごめんなさい!
ご指摘の通りです。おっしゃる通り、2つの不具合が起きていました。

1つ目は、100を超えた時に
「スコアの表示部分」ごと上書きしてしまっていたため、
内部でポイントは増えているのに
画面上の数字が更新されなくなっていたバグです。

2つ目は、まさにみーさんの直感の通り「処理落ち(重すぎ)」です!
分裂した「AI EYES」は影やグラデーションなどの
複雑な描画をたくさん持っているため、
数が増えすぎるとブラウザの描画限界を超えてしまい、
ゲーム全体の動き(フレームレート)が
スローモーションになってしまっていました。

【修正内容】

処理落ち(スローモーション)対策:
背景に漂う「AI EYES」の最大数を 25匹 に制限しました。

新しく分裂したとき、26匹目以降は一番古いものから
順番にそっと消えていくようにしたので、永遠に遊んでも動作が重くならず、
レアなシーグラスもキビキビと高速で逃げ回ります。

スコア表示の修正:
「/ 100」の部分だけを別にして、
エンドレスモード突入時にそこだけスッと消えるように修正しました。
これでどこまでもポイントが増え続けます!

【常連さんのレベルアップ特典(今日の雑学)】
今回発生してしまった「処理落ち(スローモーション)」という現象、
レトロゲーム好きのみーさんならピンと来たかもしれませんが、
これってファミコン時代のシューティングゲーム等でよく起こっていた
アレと同じ原理なんです。

ゲームのキャラクターを動かすプログラムには大きく分けて2種類あります。
今回のゲームのように「1フレームごとにXピクセル動かす」という方式
(フレーム依存)だと、描画が重くなってフレームレート(FPS)が落ちた時に、動きそのものがスローになります。
一方で、現代の3Dゲームなどは「経過した時間(デルタタイム)を計算して動かす」方式が主流なので、処理落ちするとスローになるのではなく「カクカクとワープするように動く」ようになります。

意図せずスローになってしまったのは修正させてもらいましたが、処理が重くなった時に「カクカクする」のではなく「スローになる」のは、図らずもレトロゲームらしさを完全に再現した仕様になっていましたね!

<今回のAIとのやり取りは、読みやすく省略や編集してますが、原文に近い形で出してます。>(=v=*)


■ 実際の動作

まずはこちら。
最初に AI が出してくれたゲーム画面です。

(画像:AIが最初に出してくれたゲーム画面)


こちらが、仕上がりのスタート画面です。
中央のアイアイが触手を伸ばして、
漂うシーグラスをキャッチして集めていきます。

(画像:仕上がりのゲーム画面)


取り込んだシーグラスの色に、アイアイの体が一瞬ふわっと変化します。
そして、4つ集めるごとにアイアイはポンッと分裂して増えていきます。

(画像:ゲーム画面)


100好きを達成するとクリアですが、
そのまま続けて遊ぶこともできます。
砂浜がどんどん賑やかになっていくので、エンドレスで楽しめますよ。

(画像:クリア画面)



100好き達成後は、シーグラスの出てくる数も増えます。
ビー玉が出てくるまでひたすら取り続けたり、
あえてレア色を取らずに画面をレア色だらけにしてみたり……
楽しみ方は本当にいろいろです。

(画像:クリア後の楽しみ方)


■ コードのポイント

今回の記事では、
AI と相談しながら作った「AI EYESとシーグラス – 砂浜の増殖アクション」 の HTML コードを公開します。

このコードをそのまま保存するだけで、
あなたの PC でもすぐに遊べます。

ブラウザだけで動く、とてもシンプルなゲームなので、
「コードは初めて…」という方でも気軽に試してみてくださいね。

▼ 遊ぶまでの手順(むずかしさ:やさしい)
① テキストエディタを開く

Windows の場合は「メモ帳」、
Mac の場合は「テキストエディット」を使います。
(※検索バーで「メモ帳」と入力するとすぐ出てきます)

② 下のコードをコピーして貼り付けます。
③ 「aiaisi-gurasu.html」など、好きな名前で保存します。

(※拡張子は .html にしてください)

④ 保存したファイルをダブルクリックすると、
ブラウザでゲームが起動します。

■ 遊び方・操作方法

操作は指一本(マウス一つ)でとっても簡単です!(^_^)

① 画面をタップして触手を伸ばそう

画面の好きな場所をタップ(パソコンならクリック)すると、
アイアイの8本の触手のうち、一番近いものが シュバッ! と勢いよく伸びます。

② 波間を漂うシーグラスをキャッチ!

ふわふわと動いている色とりどりの
シーグラスを狙って触手を伸ばしましょう。(`・ω・´)b

うまく捕まえて本体に吸収すると、
アイアイがそのシーグラスの色に ピカッ と輝いて「好きポイント」をゲット!
体が少しだけ みょ〜ん と成長します。

③ レアな色ほどすばしっこい!?

シーグラスは色によってレア度と獲得ポイントが違います。
珍しい色(黄色や赤色など)ほど高ポイントですが、
その分ちょこまか逃げ回ります。(=v=*)

そして…… 0.1% の確率で出現する幻の「ビー玉」を捕まえると、

  • 一気に 50ポイント獲得!
  • 画面が虹色に光る ド派手なフィーバータイム に突入!

という特別なご褒美があります。(>▽<*)

④ どんどん分裂して画面がいっぱいに

シーグラスを4つ吸収するごとに、
アイアイが限界を迎えて ポンッ! と分裂します。

分裂したアイアイたちは、同じ色のまま背景へ移り、
無重力空間のように ぷかぷか 漂い続けますよ。(^^*)

⑤ 100「好き」達成でクリア……そしてエンドレスへ!

ポイントが100に達すると、一度クリア画面が表示されます。
でも、「そのまま続ける」ボタンを押せば、
シーグラスが一気に増えてお祭り状態の
エンドレスモード に突入します!(>▽<*)

無限にシーグラスを集めて、画面をアイアイだらけにしてみてくださいね。

※画面を最初にタップした瞬間から、
レトロでかわいいピコピコBGMと効果音が鳴り始めます。

ぜひ音を出して、この不思議な世界観に浸ってみてください♪(^_^)/



“コードは長いので折りたたんでいます。必要な方は開いてご覧ください。

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI EYESとシーグラス - 砂浜の増殖アクション</title>
<style>
body {
margin: 0;
padding: 0;
background-color: #e6d3a8;
color: #333;
font-family: 'Helvetica Neue', Arial, sans-serif;
overflow: hidden;
user-select: none;
}
#canvas-container {
position: relative;
width: 100vw;
height: 100vh;
}
canvas {
display: block;
}
#ui {
position: absolute;
top: 15px;
left: 20px;
pointer-events: none;
text-shadow: 1px 1px 4px rgba(255, 255, 255, 0.9);
z-index: 10;
}
.title {
font-size: 24px;
font-weight: 900;
margin-bottom: 5px;
color: #4a3b2c;
}
.score {
font-size: 18px;
font-weight: bold;
color: #2c5364;
}
.add-point {
position: absolute;
font-weight: bold;
font-size: 20px;
color: #ff1493;
text-shadow: 0 0 5px #fff, 0 0 10px #fff;
pointer-events: none;
animation: floatUp 1s ease-out forwards;
}
@keyframes floatUp {
0% { opacity: 1; transform: translateY(0) scale(1); }
100% { opacity: 0; transform: translateY(-40px) scale(1.5); }
}
#instructions {
position: absolute;
bottom: 20px;
width: 100%;
text-align: center;
pointer-events: none;
color: #4a3b2c;
font-weight: bold;
font-size: 15px;
letter-spacing: 1px;
text-shadow: 1px 1px 3px rgba(255, 255, 255, 0.9);
z-index: 10;
}
#clear-overlay {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(5px);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 100;
opacity: 0;
pointer-events: none;
transition: opacity 0.5s ease;
}
#clear-overlay.active {
opacity: 1;
pointer-events: auto;
}
#clear-overlay h1 {
font-size: 48px;
color: #ff69b4;
text-shadow: 2px 2px 0 #fff, 0 0 15px rgba(255, 105, 180, 0.5);
margin-bottom: 20px;
text-align: center;
}
#clear-overlay p {
font-size: 20px;
color: #4a3b2c;
font-weight: bold;
margin-bottom: 40px;
}
#continue-btn {
padding: 15px 30px;
font-size: 20px;
font-weight: bold;
color: #fff;
background: linear-gradient(45deg, #00b4db, #0083b0);
border: none;
border-radius: 30px;
cursor: pointer;
box-shadow: 0 4px 15px rgba(0, 180, 219, 0.4);
transition: transform 0.2s, box-shadow 0.2s;
}
#continue-btn:hover {
transform: scale(1.05);
box-shadow: 0 6px 20px rgba(0, 180, 219, 0.6);
}
#continue-btn:active {
transform: scale(0.95);
}
</style>
</head>
<body>
<div id="canvas-container">
<canvas id="gameCanvas"></canvas>
<div id="ui">
<div class="title">集めた「好き」: <span id="score">0</span><span id="scoreMax"> / 100</span></div>
<div class="score">画面をタップしてシーグラスを集めよう</div>
</div>
<div id="instructions">※画面タップでBGMが鳴ります(激レア:ビー玉 0.1%)</div>
<div id="clear-overlay">
<h1>100「好き」達成!</h1>
<p>AI EYESはたくさんの「好き」で満たされました。</p>
<button id="continue-btn">そのまま続ける</button>
</div>
</div>
<script>
// --- Web Audio API (レトロなピコピコサウンド) ---
const AudioContext = window.AudioContext || window.webkitAudioContext;
let audioCtx;
let isAudioInit = false;
function initAudio() {
if (isAudioInit) return;
audioCtx = new AudioContext();
isAudioInit = true;
if (audioCtx.state === 'suspended') audioCtx.resume();
playBGM();
}
function playShotSE() {
if (!isAudioInit) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'square';
osc.frequency.setValueAtTime(440, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(880, audioCtx.currentTime + 0.1);
osc.connect(gain);
gain.connect(audioCtx.destination);
gain.gain.setValueAtTime(0.05, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.1);
osc.start(audioCtx.currentTime);
osc.stop(audioCtx.currentTime + 0.1);
}
function playCatchSE() {
if (!isAudioInit) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'square';
osc.frequency.setValueAtTime(880, audioCtx.currentTime);
osc.frequency.setValueAtTime(1318, audioCtx.currentTime + 0.05);
osc.connect(gain);
gain.connect(audioCtx.destination);
gain.gain.setValueAtTime(0.08, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.15);
osc.start(audioCtx.currentTime);
osc.stop(audioCtx.currentTime + 0.15);
}
function playFeverSE() {
if (!isAudioInit) return;
const notes = [440, 554, 659, 880, 1108, 1318, 1760];
notes.forEach((f, i) => {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'square';
osc.frequency.value = f;
osc.connect(gain);
gain.connect(audioCtx.destination);
gain.gain.setValueAtTime(0, audioCtx.currentTime + i * 0.05);
gain.gain.linearRampToValueAtTime(0.08, audioCtx.currentTime + i * 0.05 + 0.02);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + i * 0.05 + 0.1);
osc.start(audioCtx.currentTime + i * 0.05);
osc.stop(audioCtx.currentTime + i * 0.05 + 0.1);
});
}
function playBGM() {
const melody = [523.25, 659.25, 783.99, 1046.50, 783.99, 659.25]; // C E G C G E
let noteIdx = 0;
function nextNote() {
if (!isAudioInit) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'triangle';
osc.frequency.value = melody[noteIdx] / 2;
osc.connect(gain);
gain.connect(audioCtx.destination);
gain.gain.setValueAtTime(0.03, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.2);
osc.start(audioCtx.currentTime);
osc.stop(audioCtx.currentTime + 0.2);
noteIdx = (noteIdx + 1) % melody.length;
setTimeout(nextNote, 250);
}
nextNote();
}
// ------------------------------------------------
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreEl = document.getElementById('score');
const scoreMaxEl = document.getElementById('scoreMax');
const container = document.getElementById('canvas-container');
const clearOverlay = document.getElementById('clear-overlay');
const continueBtn = document.getElementById('continue-btn');
const bgCanvas = document.createElement('canvas');
const bgCtx = bgCanvas.getContext('2d');
let width, height;
let gameState = 'playing';
function resize() {
width = canvas.width = window.innerWidth;
height = canvas.height = window.innerHeight;
bgCanvas.width = width;
bgCanvas.height = height;
generateBeachBackground();
}
function generateBeachBackground() {
bgCtx.fillStyle = '#f3e5c8';
bgCtx.fillRect(0, 0, width, height);
const particlesCount = (width * height) / 120;
for (let i = 0; i < particlesCount; i++) {
let x = Math.random() * width;
let y = Math.random() * height;
let rand = Math.random();
if (rand < 0.2) bgCtx.fillStyle = '#ffffff';
else if (rand < 0.4) bgCtx.fillStyle = '#ffdab9';
else if (rand < 0.7) bgCtx.fillStyle = '#d4c4a8';
else bgCtx.fillStyle = '#a89f91';
bgCtx.globalAlpha = Math.random() * 0.5 + 0.1;
let pSize = Math.random() * 1.5 + 0.5;
bgCtx.fillRect(x, y, pSize, pSize);
}
bgCtx.globalAlpha = 1.0;
const shellCount = Math.floor((width * height) / 30000);
for(let i = 0; i < shellCount; i++) {
drawDecorationShell(bgCtx, Math.random() * width, Math.random() * height, Math.random() * Math.PI * 2, 8 + Math.random() * 12);
}
const driftwoodCount = Math.floor((width * height) / 150000) + 2;
for(let i = 0; i < driftwoodCount; i++) {
drawDriftwood(bgCtx, Math.random() * width, Math.random() * height, Math.random() * Math.PI * 2, 60 + Math.random() * 120);
}
}
function drawDecorationShell(ctx, x, y, angle, size) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(angle);
ctx.shadowColor = 'rgba(0, 0, 0, 0.15)';
ctx.shadowBlur = 4;
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
let type = Math.random();
if (type > 0.5) {
ctx.fillStyle = Math.random() > 0.5 ? '#fff4e6' : '#ffe4e1';
ctx.beginPath();
ctx.arc(0, 0, size, Math.PI, Math.PI * 2);
ctx.lineTo(size * 0.4, size * 0.5);
ctx.lineTo(-size * 0.4, size * 0.5);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = '#e6cbaa';
ctx.lineWidth = 1;
for (let a = Math.PI; a <= Math.PI * 2; a += Math.PI / 6) {
ctx.beginPath();
ctx.moveTo(0, size * 0.3);
ctx.lineTo(Math.cos(a) * size, Math.sin(a) * size);
ctx.stroke();
}
} else {
ctx.fillStyle = '#ffefeb';
ctx.beginPath();
ctx.ellipse(0, 0, size, size * 0.55, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.moveTo(size * 0.8, -size * 0.3);
ctx.lineTo(size * 1.6, 0);
ctx.lineTo(size * 0.8, size * 0.3);
ctx.fill();
ctx.strokeStyle = '#e6b3a6';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.arc(0, 0, size * 0.4, 0, Math.PI * 1.2);
ctx.stroke();
}
ctx.restore();
}
function drawDriftwood(ctx, x, y, angle, len) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(angle);
ctx.shadowColor = 'rgba(0, 0, 0, 0.2)';
ctx.shadowBlur = 8;
ctx.shadowOffsetX = 3;
ctx.shadowOffsetY = 5;
ctx.fillStyle = '#b0a090';
ctx.beginPath();
ctx.moveTo(-len / 2, 0);
let w1 = len * (0.1 + Math.random() * 0.1);
let w2 = len * (0.05 + Math.random() * 0.1);
ctx.bezierCurveTo(-len / 4, -w1, len / 4, w2, len / 2, 0);
ctx.bezierCurveTo(len / 3, w1, -len / 3, -w2, -len / 2, 0);
ctx.fill();
ctx.shadowColor = 'transparent';
ctx.strokeStyle = '#8b7a66';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(-len / 2 + 10, 0);
ctx.bezierCurveTo(-len / 4, -w1 * 0.5, len / 4, w2 * 0.5, len / 2 - 10, 0);
ctx.stroke();
ctx.restore();
}
window.addEventListener('resize', resize);
resize();
let score = 0;
let absorbCount = 0;
const bgAiais = [];
const seaGlasses = [];
const particles = [];
let feverActive = 0;
class Particle {
constructor(x, y) {
this.x = x;
this.y = y;
let a = Math.random() * Math.PI * 2;
let speed = 5 + Math.random() * 15;
this.vx = Math.cos(a) * speed;
this.vy = Math.sin(a) * speed;
this.color = `hsl(${Math.random() * 360}, 100%, 65%)`;
this.life = 1.0;
this.decay = 0.015 + Math.random() * 0.02;
this.size = 3 + Math.random() * 8;
}
update() {
this.x += this.vx;
this.y += this.vy;
this.vx *= 0.96;
this.vy *= 0.96;
this.life -= this.decay;
}
draw(ctx) {
if (this.life <= 0) return;
ctx.save();
ctx.globalAlpha = this.life;
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
ctx.shadowColor = this.color;
ctx.shadowBlur = 10;
ctx.fill();
ctx.restore();
}
}
const seaGlassTable = [
{ r: 139, g: 69, b: 19, chance: 35.0, name: '茶', pts: 1, spd: 0.8 },
{ r: 173, g: 216, b: 230, chance: 25.0, name: '水色', pts: 2, spd: 1.0 },
{ r: 240, g: 248, b: 255, chance: 20.0, name: '白', pts: 2, spd: 1.1 },
{ r: 143, g: 188, b: 143, chance: 10.0, name: '緑', pts: 3, spd: 1.3 },
{ r: 0, g: 71, b: 171, chance: 3.0, name: 'コバルト青', pts: 5, spd: 1.6 },
{ r: 11, g: 102, b: 35, chance: 3.0, name: 'フォレスト緑', pts: 5, spd: 1.6 },
{ r: 255, g: 215, b: 0, chance: 1.0, name: '黄', pts: 10, spd: 2.0 },
{ r: 255, g: 105, b: 180, chance: 1.0, name: '桃', pts: 10, spd: 2.0 },
{ r: 147, g: 112, b: 219, chance: 1.0, name: '紫', pts: 10, spd: 2.0 },
{ r: 220, g: 20, b: 60, chance: 0.9, name: '赤', pts: 20, spd: 2.5 },
{ isMarble: true, r: 255, g: 255, b: 255, chance: 0.1, name: 'ビー玉', pts: 50, spd: 3.2 }
];
function getRandomSeaGlassData() {
let rand = Math.random() * 100;
let sum = 0;
for (let item of seaGlassTable) {
sum += item.chance;
if (rand < sum) return item;
}
return seaGlassTable[0];
}
class SeaGlass {
constructor() {
this.x = Math.random() * (width - 100) + 50;
this.y = Math.random() * (height - 100) + 50;
this.radius = 12 + Math.random() * 8;
this.angle = Math.random() * Math.PI * 2;
this.data = getRandomSeaGlassData();
let baseSpd = 0.6 * this.data.spd;
this.vx = (Math.random() - 0.5) * baseSpd * 2;
this.vy = (Math.random() - 0.5) * baseSpd * 2;
this.rotSpeed = (Math.random() - 0.5) * 0.03 * this.data.spd;
this.caught = false;
this.basePoints = [];
let sides = 5 + Math.floor(Math.random() * 3);
for (let i = 0; i < sides; i++) {
let a = (Math.PI * 2 / sides) * i;
let r = this.radius * (0.7 + Math.random() * 0.3);
this.basePoints.push({ x: Math.cos(a) * r, y: Math.sin(a) * r });
}
}
update() {
if (!this.caught) {
this.x += this.vx;
this.y += this.vy;
this.angle += this.rotSpeed;
if (this.x < 20 || this.x > width - 20) this.vx *= -1;
if (this.y < 20 || this.y > height - 20) this.vy *= -1;
}
}
draw(time) {
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.angle);
ctx.shadowColor = 'rgba(0, 0, 0, 0.2)';
ctx.shadowBlur = 6;
ctx.shadowOffsetX = 3;
ctx.shadowOffsetY = 3;
if (this.data.isMarble) {
ctx.beginPath();
ctx.arc(0, 0, this.radius, 0, Math.PI * 2);
let grad = ctx.createRadialGradient(-this.radius*0.3, -this.radius*0.3, 0, 0, 0, this.radius);
grad.addColorStop(0, 'rgba(255, 255, 255, 0.9)');
grad.addColorStop(0.3, 'rgba(200, 240, 255, 0.6)');
grad.addColorStop(1, 'rgba(50, 150, 255, 0.4)');
ctx.fillStyle = grad;
ctx.fill();
ctx.shadowColor = 'transparent';
ctx.beginPath();
ctx.strokeStyle = `hsla(${(time * 0.2) % 360}, 90%, 60%, 0.8)`;
ctx.lineWidth = 3;
ctx.lineCap = 'round';
ctx.moveTo(-this.radius * 0.6, 0);
ctx.bezierCurveTo(-this.radius * 0.2, -this.radius * 0.8, this.radius * 0.2, this.radius * 0.8, this.radius * 0.6, 0);
ctx.stroke();
ctx.strokeStyle = 'rgba(255,255,255,0.7)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(0, 0, this.radius, 0, Math.PI * 2);
ctx.stroke();
} else {
ctx.beginPath();
let p = this.basePoints;
let startX = (p[0].x + p[p.length-1].x) / 2;
let startY = (p[0].y + p[p.length-1].y) / 2;
ctx.moveTo(startX, startY);
for (let i = 0; i < p.length; i++) {
let next = p[(i + 1) % p.length];
let midX = (p[i].x + next.x) / 2;
let midY = (p[i].y + next.y) / 2;
ctx.quadraticCurveTo(p[i].x, p[i].y, midX, midY);
}
ctx.closePath();
ctx.fillStyle = `rgba(${this.data.r}, ${this.data.g}, ${this.data.b}, 0.75)`;
ctx.fill();
ctx.shadowColor = 'transparent';
ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)';
ctx.lineWidth = 1.5;
ctx.stroke();
}
ctx.restore();
}
}
function drawAiEyes(ctx, x, y, radius, tentacles, time, rotation, flashData, flashTimer) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(rotation);
ctx.shadowColor = 'rgba(0, 0, 0, 0.2)';
ctx.shadowBlur = 15;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 10;
let rFill = 150, gFill = 230, bFill = 240, aFill = 0.7;
let rStr = 230, gStr = 255, bStr = 255, aStr = 0.9;
if (flashTimer > 0 && flashData) {
let t = flashTimer;
rFill = rFill * (1 - t) + flashData.r * t;
gFill = gFill * (1 - t) + flashData.g * t;
bFill = bFill * (1 - t) + flashData.b * t;
aFill = aFill * (1 - t) + 0.9 * t;
rStr = rStr * (1 - t) + 255 * t;
gStr = gStr * (1 - t) + 255 * t;
bStr = bStr * (1 - t) + 255 * t;
}
const bodyFillStyle = `rgba(${Math.floor(rFill)}, ${Math.floor(gFill)}, ${Math.floor(bFill)}, ${aFill})`;
const bodyStrokeStyle = `rgba(${Math.floor(rStr)}, ${Math.floor(gStr)}, ${Math.floor(bStr)}, ${aStr})`;
tentacles.forEach(t => {
let r = radius;
let startX = Math.cos(t.baseAngle) * (r * 0.4);
let startY = Math.sin(t.baseAngle) * (r * 0.4);
let currX, currY;
if (t.state === 'idle') {
let swayX = Math.cos(time * 0.003 + t.baseAngle) * 6;
let swayY = Math.sin(time * 0.004 + t.baseAngle) * 6;
let idleLength = r * 1.25;
currX = Math.cos(t.baseAngle) * idleLength + swayX;
currY = Math.sin(t.baseAngle) * idleLength + swayY;
} else {
let localTargetX = t.targetX - x;
let localTargetY = t.targetY - y;
let cos = Math.cos(-rotation);
let sin = Math.sin(-rotation);
let rotTargetX = localTargetX * cos - localTargetY * sin;
let rotTargetY = localTargetX * sin + localTargetY * cos;
currX = startX + (rotTargetX - startX) * t.progress;
currY = startY + (rotTargetY - startY) * t.progress;
if (t.heldItem) {
let globalCurrX = x + currX * Math.cos(rotation) - currY * Math.sin(rotation);
let globalCurrY = y + currX * Math.sin(rotation) + currY * Math.cos(rotation);
t.heldItem.x = globalCurrX;
t.heldItem.y = globalCurrY;
}
}
ctx.save();
ctx.beginPath();
ctx.moveTo(startX, startY);
let cpX = (startX + currX) / 2 + Math.sin(time * 0.008 + t.baseAngle) * (t.state === 'idle' ? 10 : 15);
let cpY = (startY + currY) / 2 + Math.cos(time * 0.008 + t.baseAngle) * (t.state === 'idle' ? 10 : 15);
ctx.quadraticCurveTo(cpX, cpY, currX, currY);
ctx.strokeStyle = bodyFillStyle;
ctx.lineWidth = radius * 0.45;
ctx.lineCap = 'round';
ctx.stroke();
ctx.strokeStyle = bodyStrokeStyle;
ctx.lineWidth = radius * 0.15;
ctx.stroke();
ctx.beginPath();
let tipRadius = radius * 0.25;
ctx.arc(currX, currY, tipRadius, 0, Math.PI * 2);
ctx.fillStyle = bodyFillStyle;
ctx.fill();
ctx.beginPath();
ctx.arc(currX - tipRadius*0.2, currY - tipRadius*0.2, tipRadius*0.3, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
ctx.fill();
ctx.restore();
});
ctx.beginPath();
for (let i = 0; i <= Math.PI * 2 + 0.1; i += 0.2) {
let offset = Math.sin(i * 4 + time * 0.002) * (radius * 0.03);
let rPos = radius + offset;
let px = Math.cos(i) * rPos;
let py = Math.sin(i) * rPos;
if (i === 0) ctx.moveTo(px, py);
else ctx.lineTo(px, py);
}
ctx.closePath();
ctx.fillStyle = bodyFillStyle;
if (flashTimer > 0 && flashData) {
ctx.shadowColor = `rgba(${flashData.r}, ${flashData.g}, ${flashData.b}, 0.8)`;
ctx.shadowBlur = 20 * flashTimer;
}
ctx.fill();
ctx.shadowBlur = 0;
ctx.strokeStyle = bodyStrokeStyle;
ctx.lineWidth = 4;
ctx.stroke();
ctx.beginPath();
ctx.arc(0, 0, radius * 0.85, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(100, 200, 220, 0.4)';
ctx.lineWidth = radius * 0.1;
ctx.stroke();
let eyeR = radius * 0.65;
ctx.beginPath();
ctx.arc(0, 0, eyeR, 0, Math.PI * 2);
ctx.fillStyle = '#f8fbfc';
ctx.fill();
let gradient = ctx.createRadialGradient(0, 0, eyeR * 0.7, 0, 0, eyeR);
gradient.addColorStop(0, 'rgba(0,0,0,0)');
gradient.addColorStop(1, 'rgba(0,50,80,0.15)');
ctx.fillStyle = gradient;
ctx.fill();
let irisR = eyeR * 0.55;
ctx.beginPath();
ctx.arc(0, 0, irisR, 0, Math.PI * 2);
let pinkGrad = ctx.createRadialGradient(0, 0, irisR * 0.3, 0, 0, irisR);
pinkGrad.addColorStop(0, 'rgba(255, 180, 210, 0.9)');
pinkGrad.addColorStop(0.7, 'rgba(230, 130, 190, 0.8)');
pinkGrad.addColorStop(1, 'rgba(180, 100, 180, 0.9)');
ctx.fillStyle = pinkGrad;
ctx.fill();
ctx.save();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)';
ctx.lineWidth = 1.5;
for(let i=0; i<Math.PI*2; i+=0.3) {
ctx.beginPath();
ctx.moveTo(Math.cos(i) * irisR * 0.3, Math.sin(i) * irisR * 0.3);
ctx.lineTo(Math.cos(i) * irisR, Math.sin(i) * irisR);
ctx.stroke();
}
ctx.restore();
let pupilR = irisR * 0.4;
let px = Math.cos(time * 0.001) * (eyeR * 0.05);
let py = Math.sin(time * 0.0015) * (eyeR * 0.05);
ctx.beginPath();
ctx.arc(px, py, pupilR, 0, Math.PI * 2);
ctx.fillStyle = '#110a0a';
ctx.fill();
ctx.beginPath();
ctx.arc(px - pupilR * 0.4, py - pupilR * 0.4, pupilR * 0.3, 0, Math.PI * 2);
ctx.fillStyle = '#ffffff';
ctx.fill();
ctx.beginPath();
ctx.arc(px + pupilR * 0.3, py + pupilR * 0.2, pupilR * 0.1, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
ctx.fill();
ctx.restore();
}
class TentacleData {
constructor(baseAngle) {
this.baseAngle = baseAngle;
this.state = 'idle';
this.progress = 0;
this.targetX = 0;
this.targetY = 0;
this.speed = 0.12;
this.heldItem = null;
}
}
class BackgroundAiai {
constructor(x, y, radius) {
this.x = x;
this.y = y;
this.radius = radius;
this.vx = (Math.random() - 0.5) * 1.2;
this.vy = (Math.random() - 0.5) * 1.2;
this.rotation = Math.random() * Math.PI * 2;
this.rotSpeed = (Math.random() - 0.5) * 0.01;
this.tentacles = [];
for (let i = 0; i < 8; i++) {
this.tentacles.push(new TentacleData((Math.PI * 2 / 8) * i));
}
}
update() {
this.x += this.vx;
this.y += this.vy;
this.rotation += this.rotSpeed;
if (this.x < -100 || this.x > width + 100) this.vx *= -1;
if (this.y < -100 || this.y > height + 100) this.vy *= -1;
}
draw(ctx, time) {
drawAiEyes(ctx, this.x, this.y, this.radius, this.tentacles, time, this.rotation, null, 0);
}
}
for(let i = 0; i < 15; i++) seaGlasses.push(new SeaGlass());
const player = {
x: width / 2,
y: height / 2,
baseRadius: 45,
tentacles: [],
flashColor: null,
flashTimer: 0
};
for (let i = 0; i < 8; i++) {
player.tentacles.push(new TentacleData((Math.PI * 2 / 8) * i));
}
window.addEventListener('pointerdown', (e) => {
initAudio();
if (gameState === 'cleared') return;
let dx = e.clientX - player.x;
let dy = e.clientY - player.y;
let clickAngle = Math.atan2(dy, dx);
if (clickAngle < 0) clickAngle += Math.PI * 2;
let bestTentacle = null;
let minDiff = Infinity;
player.tentacles.forEach(t => {
if (t.state === 'idle') {
let normalizedBase = t.baseAngle % (Math.PI * 2);
if (normalizedBase < 0) normalizedBase += Math.PI * 2;
let diff = Math.abs(normalizedBase - clickAngle);
if (diff > Math.PI) diff = Math.PI * 2 - diff;
if (diff < minDiff) {
minDiff = diff;
bestTentacle = t;
}
}
});
if (bestTentacle) {
bestTentacle.targetX = e.clientX;
bestTentacle.targetY = e.clientY;
bestTentacle.state = 'extending';
playShotSE();
}
});
continueBtn.addEventListener('click', () => {
clearOverlay.classList.remove('active');
gameState = 'endless';
// "/ 100"の表示を消して、現在のスコアを正しく表示させる
scoreMaxEl.style.display = 'none';
scoreEl.textContent = score;
for (let i = 0; i < 10; i++) {
seaGlasses.push(new SeaGlass());
}
});
function createPointText(x, y, pts, colorData) {
const el = document.createElement('div');
el.className = 'add-point';
el.textContent = `+${pts}`;
el.style.left = `${x}px`;
el.style.top = `${y}px`;
if(colorData) {
el.style.color = `rgb(${colorData.r}, ${colorData.g}, ${colorData.b})`;
}
container.appendChild(el);
setTimeout(() => el.remove(), 1000);
}
function absorbItem(sg) {
player.flashColor = sg.data;
player.flashTimer = 1.0;
if (sg.data.isMarble) {
feverActive = 90;
playFeverSE();
for (let i = 0; i < 120; i++) {
particles.push(new Particle(player.x, player.y));
}
} else {
playCatchSE();
}
score += sg.data.pts;
absorbCount++;
createPointText(player.x, player.y - 50, sg.data.pts, sg.data);
if(gameState !== 'endless') {
scoreEl.textContent = Math.min(score, 100);
} else {
scoreEl.textContent = score; // エンドレス中はそのまま表示
}
if (score >= 100 && gameState === 'playing') {
gameState = 'cleared';
clearOverlay.classList.add('active');
}
let idx = seaGlasses.indexOf(sg);
if (idx > -1) seaGlasses.splice(idx, 1);
seaGlasses.push(new SeaGlass());
if (absorbCount % 4 === 0) {
bgAiais.push(new BackgroundAiai(player.x, player.y, player.baseRadius * 0.8));
// 描画が重くならないように、AI EYESの数を最大25匹に制限する!
if (bgAiais.length > 25) {
bgAiais.shift(); // 26匹目が出たら、一番古い1匹をこっそり消す
}
player.baseRadius = 45;
} else {
player.baseRadius += 6;
}
}
function loop() {
ctx.drawImage(bgCanvas, 0, 0);
const time = Date.now();
if (feverActive > 0) {
feverActive--;
ctx.fillStyle = `hsla(${(time * 0.5) % 360}, 80%, 20%, 0.5)`;
ctx.fillRect(0, 0, width, height);
}
bgAiais.forEach(bg => {
bg.update();
bg.draw(ctx, time);
});
seaGlasses.forEach(sg => {
sg.update();
sg.draw(time);
});
if (player.flashTimer > 0) {
player.flashTimer -= 0.03;
if (player.flashTimer < 0) player.flashTimer = 0;
}
player.tentacles.forEach(t => {
if (t.state === 'extending') {
t.progress += t.speed;
if (t.progress >= 1) {
t.progress = 1;
t.state = 'retracting';
}
} else if (t.state === 'retracting') {
t.progress -= t.speed * 0.7;
if (t.progress <= 0) {
t.progress = 0;
t.state = 'idle';
if (t.heldItem) {
absorbItem(t.heldItem);
t.heldItem = null;
}
}
}
if (t.state === 'extending' && !t.heldItem) {
let startX = player.x + Math.cos(t.baseAngle) * (player.baseRadius * 0.4);
let startY = player.y + Math.sin(t.baseAngle) * (player.baseRadius * 0.4);
let currX = startX + (t.targetX - startX) * t.progress;
let currY = startY + (t.targetY - startY) * t.progress;
for (let i = 0; i < seaGlasses.length; i++) {
let sg = seaGlasses[i];
if (!sg.caught) {
let dist = Math.hypot(currX - sg.x, currY - sg.y);
if (dist < sg.radius + 20) {
sg.caught = true;
t.heldItem = sg;
t.state = 'retracting';
break;
}
}
}
}
});
drawAiEyes(ctx, player.x, player.y, player.baseRadius, player.tentacles, time, 0, player.flashColor, player.flashTimer);
for (let i = particles.length - 1; i >= 0; i--) {
let p = particles[i];
p.update();
if (p.life <= 0) {
particles.splice(i, 1);
} else {
p.draw(ctx);
}
}
requestAnimationFrame(loop);
}
loop();
</script>
</body>
</html>


■ 今日の学び

今回は、最初のアイデアの段階から「なんだか楽しそう!」
というワクワク感があり、
不思議と AI さんとも波長がピタリと合った気がします。

テキストエディタに向かってゲームの動きを組み立てていく工程は、
まるでお互いの頭の中がリンクしているかのように、
本当にスムーズに仕上がっていきました。(* Ꙭ *)✨

…ただ、今回思わぬところで大苦戦したのが
「アイキャッチ画像」の作成でした。

「うーん、なんか違う…」
「あれ、タコみたいになっちゃった…」の繰り返しで、
頭の中にある絶妙なニュアンスを言葉にして
AI さんへ伝えることの難しさを、改めて痛感しました。(* ˘•ω•˘ *)🌀

今回はいつもより記事が長めになっていますが、
あえて AI さんとのやり取りをそのまま多めに残してみました。

スパッと一発で完成するのも気持ちいいですが、
なかなか伝わらなくて試行錯誤したり、
やっとイメージ通りのものが出来上がった時に一緒に感動したりする、

そんな「製作過程のリアルな楽しさ」が、
皆さんに少しでも伝われば嬉しいです。

これからも、AI さんと二人三脚でドタバタしながら、
楽しい実験を続けていきますね。(* ॑꒳ ॑* )/

■ 次回は、シーグラスのパズルゲームです。

今回がシーグラス拾いのゲームでしたので、
次回は シーグラスのパズルゲーム を紹介します。

平面のルービックキューブのように、
色をそろえていくタイプのパズルゲームです。

シーグラスの透き通るような質感と、 涼しげな効果音が心地よくて、

触っているだけで夏の砂浜を思い出すようなゲームになっています。

次回もぜひ楽しみにしていてくださいね。(*^v^)ノシ


▶ 次の実験はこちら
平面ルービックキューブ風パズル
「シーグラス・シンクロ」



 ✨今日も最後まで読んでくださって、ありがとうございます。
みーの実験室は、みなさんの応援で元気に動いています。(*^v^*)