Z

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Z – 次世代ソーシャルネットワーク</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/aframe/1.5.0/aframe.min.js"></script>
<style>
:root{
  --primary:#1DA1F2;--background:#fff;--text:#000;--border:#E1E8ED;--card:#F7F9F9;--danger:#E0245E;
}
[data-theme="dark"]{--background:#15202B;--text:#fff;--border:#38444D;--card:#192734}
*{box-sizing:border-box;margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}
body{background:var(--background);color:var(--text);min-height:100vh;transition:.3s}
.hidden{display:none}
.wrapper{max-width:640px;margin-inline:auto;padding:20px}
.timeline{margin-top:2rem}
.timeline-item{background:var(--card);border-radius:12px;padding:1rem;margin-bottom:1rem;box-shadow:0 2px 6px rgba(0,0,0,.05)}
.timeline-item h3{margin:0 0 .5rem;font-size:1.1rem}
.timeline-item p{margin:0;white-space:pre-wrap;line-height:1.4}
.timeline-item small{display:block;margin-top:.5rem;font-size:.75rem;color:var(--border)}
.auth-box{background:var(--card);border-radius:12px;padding:1.5rem;margin-bottom:2rem}
.auth-box input{padding:.75rem;border:1px solid var(--border);border-radius:8px;width:100%;margin-bottom:.5rem}
.auth-box button{background:var(--primary);color:white;border:none;border-radius:8px;padding:.75rem;margin-top:.5rem;width:100%;cursor:pointer;font-weight:bold}
.profile-edit{background:var(--card);padding:1rem;border-radius:12px;margin-bottom:2rem}
.profile-edit h3{margin-bottom:.75rem}
.profile-edit input{width:100%;margin:.5rem 0;padding:.5rem;border:1px solid var(--border);border-radius:8px}
.follow-btn{background:#ccc;padding:.3rem .8rem;border-radius:8px;border:none;cursor:pointer;font-size:.85rem;margin-top:.5rem}
img.upload-preview{max-width:100px;border-radius:8px;margin-top:.5rem}
</style>
</head>
<body>
<div class="wrapper">
  <div id="authBox" class="auth-box">
    <h2>ログイン / 登録</h2>
    <input type="email" id="email" placeholder="メールアドレス">
    <input type="tel" id="phone" placeholder="電話番号">
    <input type="password" id="password" placeholder="パスワード">
    <input type="text" id="username" placeholder="ユーザー名">
    <button onclick="loginOrRegister()">ログイン / 登録</button>
  </div>
  <div id="mainBox" class="hidden">
    <h1 style="font-size:1.5rem;margin-bottom:1rem">Zタイムライン</h1>
    <div style="margin-bottom:1rem">ようこそ、<span id="userEmail"></span> さん!</div>
    <div class="profile-edit">
      <h3>プロフィール編集</h3>
      <input type="text" id="editName" placeholder="表示名を編集">
      <input type="text" id="editBio" placeholder="自己紹介を編集">
      <button onclick="saveProfile()">プロフィール保存</button>
    </div>
    <form id="timelineForm" style="display:flex;flex-direction:column;gap:.75rem;margin-bottom:2rem">
      <input id="timelineTitle" type="text" placeholder="タイトル" required>
      <textarea id="timelineContent" placeholder="投稿内容" required style="min-height:100px"></textarea>
      <input type="file" id="imageUpload" accept="image/*">
      <img id="preview" class="upload-preview hidden">
      <button type="submit">タイムラインに投稿</button>
    </form>
    <section id="timelineList" class="timeline"></section>
    <button onclick="logout()">ログアウト</button>
  </div>
</div>
<div id="vrScene" class="hidden" style="position:fixed;inset:0;z-index:9999"></div>
<button id="vrBtn" style="position:fixed;bottom:20px;right:20px;width:56px;height:56px;border-radius:50%;background:var(--primary);color:#fff;border:none;font-size:1.3rem;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 4px 12px rgba(0,0,0,.25)" onclick="enterVR()"><i class="fa-brands fa-vr-cardboard"></i></button>
<script>
let timeline = JSON.parse(localStorage.getItem('z_timeline')||'[]');
let feeds = JSON.parse(localStorage.getItem('z_feeds')||'[]');
let currentUser = JSON.parse(localStorage.getItem('z_user')||'null');
const authBox = document.getElementById('authBox');
const mainBox = document.getElementById('mainBox');
const timelineForm = document.getElementById('timelineForm');
const timelineList = document.getElementById('timelineList');
const userEmailSpan = document.getElementById('userEmail');
const previewImg = document.getElementById('preview');
const imageUpload = document.getElementById('imageUpload');
function loginOrRegister(){
  const email = document.getElementById('email').value.trim();
  const phone = document.getElementById('phone').value.trim();
  const pass = document.getElementById('password').value;
  const name = document.getElementById('username').value.trim();
  if(!email || !pass || !name){ alert('メール、パスワード、ユーザー名を入力してください'); return; }
  currentUser = {email, phone, name, bio:"", followers:[], following:[]};
  localStorage.setItem('z_user', JSON.stringify(currentUser));
  authBox.classList.add('hidden');
  mainBox.classList.remove('hidden');
  userEmailSpan.textContent = email;
  renderTimeline();
}
function logout(){ localStorage.removeItem('z_user'); location.reload(); }
function saveProfile(){
  const name = document.getElementById('editName').value;
  const bio = document.getElementById('editBio').value;
  if(name) currentUser.name = name;
  if(bio) currentUser.bio = bio;
  localStorage.setItem('z_user', JSON.stringify(currentUser));
  alert('プロフィールを保存しました');
}
function renderTimeline(){
  if(!timeline.length){ timelineList.innerHTML = '<p style="color:var(--border)">投稿がまだありません</p>'; return; }
  timelineList.innerHTML = timeline.map((t, index)=>{
    return `<div class="timeline-item">
      <h3>${t.title}</h3>
      <p>${t.content}</p>
      ${t.image ? `<img src="${t.image}" style="max-width:100%;margin-top:.5rem;border-radius:8px">` : ''}
      <small>${new Date(t.created).toLocaleString()}</small>
      <button onclick="followUser('${t.email}')" class="follow-btn">フォロー</button>
      <button onclick="deletePost(${index})" style="margin-top:.5rem;padding:.3rem .6rem;border:none;background:#eee;border-radius:6px;font-size:.8rem;cursor:pointer">削除</button>
    </div>`;
  }).join('');
}
function followUser(email){
  if(!currentUser.following.includes(email)){
    currentUser.following.push(email);
    localStorage.setItem('z_user', JSON.stringify(currentUser));
    alert(`${email} をフォローしました`);
  }
}
function deletePost(index){
  if(confirm('この投稿を削除しますか?')){
    timeline.splice(index,1);
    localStorage.setItem('z_timeline', JSON.stringify(timeline));
    renderTimeline();
  }
}
timelineForm.addEventListener('submit',e=>{
  e.preventDefault();
  const title = document.getElementById('timelineTitle').value.trim();
  const content = document.getElementById('timelineContent').value.trim();
  const file = imageUpload.files[0];
  if(!title || !content) return;
  const newPost = {title, content, image:null, created:new Date().toISOString(), email: currentUser.email};
  if(file){
    const reader = new FileReader();
    reader.onload = ()=>{
      newPost.image = reader.result;
      timeline.unshift(newPost);
      localStorage.setItem('z_timeline', JSON.stringify(timeline));
      renderTimeline();
    };
    reader.readAsDataURL(file);
  } else {
    timeline.unshift(newPost);
    localStorage.setItem('z_timeline', JSON.stringify(timeline));
    renderTimeline();
  }
  timelineForm.reset();
  previewImg.classList.add('hidden');
});
imageUpload.addEventListener('change',()=>{
  const file = imageUpload.files[0];
  if(file){
    const reader = new FileReader();
    reader.onload = ()=>{
      previewImg.src = reader.result;
      previewImg.classList.remove('hidden');
    };
    reader.readAsDataURL(file);
  }
});
function botAutoPost(){
  const phrases = ['こんにちは!', '今日も頑張ろう!', 'Zへようこそ!'];
  const msg = phrases[Math.floor(Math.random()*phrases.length)];
  timeline.unshift({title:'BOT投稿', content:msg, image:null, created:new Date().toISOString(), email:'bot@z.jp'});
  localStorage.setItem('z_timeline', JSON.stringify(timeline));
  renderTimeline();
}
setInterval(botAutoPost, 60000);
function fetchFeed(url){
  fetch(`https://api.rss2json.com/v1/api.json?rss_url=${encodeURIComponent(url)}`)
    .then(res=>res.json())
    .then(data=>{
      if(!data.items) return;
      data.items.slice(0,3).forEach(item=>{
        timeline.unshift({title:item.title, content:item.link, image:null, created:new Date().toISOString(), email:data.feed.title});
      });
      localStorage.setItem('z_timeline', JSON.stringify(timeline));
      renderTimeline();
    }).catch(e=>console.error('feed error',e));
}
feeds.forEach(fetchFeed);
function enterVR(){
  document.getElementById('vrScene').innerHTML = `
    <a-scene embedded>
      <a-sky color="#ECECEC"></a-sky>
      ${timeline.slice(0,10).map((p,i)=>`<a-entity text="value:${p.title}: ${p.content.replace(/\n/g,' ')};wrapCount:30" position="0 ${3-i*1.5} -3"></a-entity>`).join('')}
      <a-camera position="0 1.6 0"></a-camera>
    </a-scene>`;
  document.getElementById('vrScene').classList.remove('hidden');
  document.getElementById('vrBtn').classList.add('hidden');
}
document.addEventListener('keydown',e=>{
  if(e.key==='Escape' && !document.getElementById('vrScene').classList.contains('hidden')){
    document.getElementById('vrScene').classList.add('hidden');
    document.getElementById('vrBtn').classList.remove('hidden');
    document.getElementById('vrScene').innerHTML='';
  }
});
if(currentUser){
  authBox.classList.add('hidden');
  mainBox.classList.remove('hidden');
  userEmailSpan.textContent = currentUser.email;
  renderTimeline();
}
</script>
</body>
</html>

MusicPlayer.java

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;

import javazoom.jl.player.Player;     // JLayer

/** 超シンプル MP3 プレイヤー */
public class MusicPlayer extends JFrame {
    private static final long serialVersionUID = 1L;

    private JButton playBtn = new JButton("▶ 再生");
    private JButton stopBtn = new JButton("■ 停止");
    private JLabel  status  = new JLabel("ファイルを開いてください");
    private File    currentFile;
    private Player  player;           // 再生用スレッド
    private Thread  playThread;

    public MusicPlayer() {
        super("Java Swing Music Player");

        // 画面レイアウト
        JPanel ctrl = new JPanel();
        ctrl.add(playBtn);
        ctrl.add(stopBtn);
        add(ctrl, BorderLayout.CENTER);
        add(status, BorderLayout.SOUTH);

        // メニュー
        JMenuBar bar = new JMenuBar();
        JMenu     f  = new JMenu("ファイル");
        JMenuItem open = new JMenuItem("開く...");
        open.addActionListener(e -> chooseFile());
        f.add(open);
        bar.add(f);
        setJMenuBar(bar);

        // ボタン挙動
        playBtn.addActionListener(e -> play());
        stopBtn.addActionListener(e -> stop());

        // ウィンドウ設定
        setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        setSize(320, 120);
        setResizable(false);
        setLocationRelativeTo(null);
    }

    /** ファイル選択ダイアログ */
    private void chooseFile() {
        JFileChooser fc = new JFileChooser();
        fc.setFileFilter(new FileNameExtensionFilter("MP3 Audio", "mp3"));
        if (fc.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
            currentFile = fc.getSelectedFile();
            status.setText("選択中: " + currentFile.getName());
        }
    }

    /** 再生開始 */
    private void play() {
        if (currentFile == null) {
            JOptionPane.showMessageDialog(this, "まず MP3 を選択してください");
            return;
        }
        stop(); // 既に再生中なら停止
        playThread = new Thread(() -> {
            try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(currentFile))) {
                player = new Player(in);
                status.setText("再生中: " + currentFile.getName());
                player.play();                  // ブロッキング
                SwingUtilities.invokeLater(() -> status.setText("停止"));
            } catch (Exception ex) {
                ex.printStackTrace();
                SwingUtilities.invokeLater(() -> status.setText("再生失敗"));
            }
        });
        playThread.start();
    }

    /** 再生停止 */
    private void stop() {
        if (player != null) {
            player.close();
        }
        if (playThread != null) {
            playThread.interrupt();
        }
        status.setText("停止");
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> new MusicPlayer().setVisible(true));
    }
}

C:\java> dir jlayer-1.0.1.jar
2025/05/06 92,109 jlayer-1.0.1.jar ← この行が出れば OK
コンパイル
C:\java> javac -encoding UTF-8 -cp “.;jlayer-1.0.1.jar” MusicPlayer.java

Unity C# SaveManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
public class SaveManager
{
    const string location = "Assets/Resources/Data";

    public static void SaveData<T>(string fileName, T template)
    {
        if (!Directory.Exists(location))
        {
            Directory.CreateDirectory(location);
        }
        string data = JsonUtility.ToJson(template);
        string path = Path.Combine(location, fileName);
        using (FileStream stream = new FileStream(path, FileMode.Create))
        {
            using (StreamWriter writer = new StreamWriter(stream))
            {
                writer.Write(data);
            }
            stream.Close();
        }
        Debug.Log("game saved");
    }

    public static T LoadData<T>(string fileName) where T : class
    {
        T load = null;
        string read = "";
        string path = Path.Combine(location, fileName);
        if(!File.Exists(path))
        {
            return null;
        }

        using (FileStream stream=new FileStream(path,FileMode.Open))
        {
            using (StreamReader reader=new StreamReader(stream))
            {
                read = reader.ReadToEnd();
            }
            stream.Close();
        }
        load = JsonUtility.FromJson<T>(read);
            return load;
    }
}

Unity C# Stats.cs

using UnityEngine;

[System.Serializable]
public class Stats // ← MonoBehaviourを削除
{
    public int Level = 1;
    public int maxHp = 1;
    public int atk = 1;
    public int def = 1;
    public int mana = 1;
    public int manaXSecond = 5;
    public CharacterClass charClass = CharacterClass.warrior;
}

public enum CharacterClass
{
    warrior,
    maga,
    priest,
    paladin,
    shamano,
    druid,
    rogue,
    ranger
}

Unity player.cs

using Photon.Pun;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : Entity
{
    CameraFollow follow;
    [SerializeField]
    float rotSpeed = 2;
    [SerializeField]
    float scrollAmount = 3;
    [SerializeField]
    float minZoom = 10, maxZoom = 120;
    ActionController controller;
    const float second = 1;
    float manaCounter = 1;
    public SaveData data = new SaveData();
    // Start is called before the first frame update
    public override void Init()
    {
        base.Init();
        if (!photonView.IsMine) return;
        data = SaveManager.LoadData<SaveData>(data.characterName);
        if(data==null)
        {
            data = new SaveData();
        }
        controller = GetComponent<ActionController>();
        controller.sync = sync;
        controller.Init(this);
        var f = Resources.Load<CameraFollow>(StaticStrings.follow);
        follow = Instantiate(f, transform.position, transform.rotation);
        follow.Init(transform);
        WorldManager.instance.playerList.Add(transform);
        UIManager.instance.player = this;
        onDeathEvent = () =>
        {
            UIManager.instance.deathPanel.SetActive(true);
        };
    }

    public override void Tick()
    {
        UseCamera();
        if(controller.mana<stats.mana)
        {
            manaCounter -= Time.deltaTime;
            if (manaCounter<=0)
            {
                manaCounter = second;
                controller.mana += stats.manaXSecond;
                if (controller.mana > stats.mana) controller.mana = stats.mana;
            }
        }
        if (!CanMove()) return;
        float x = Input.GetAxisRaw(StaticStrings.horizontal);
        float y = Input.GetAxisRaw(StaticStrings.vertical);
        Vector3 move = (transform.right * x) + (transform.forward * y);
        move *= Time.deltaTime * moveMultipler * moveSpeed;
        move.y = rb.velocity.y;
        rb.velocity = move;
        sync.Move(x, y);
        controller.Tick(follow.transform,x,y);
    }

    void UseCamera()
    {
        float x = Input.GetAxis(StaticStrings.mouseX);
        float scroll = Input.GetAxisRaw(StaticStrings.scroll);
        Vector3 rot = follow.arm.rotation.eulerAngles;
        follow.transform.rotation = Quaternion.Euler(rot.x, rot.y + x * rotSpeed, rot.z);
        if(scroll!=0)
        {
            float val = scrollAmount * scroll;
            val += follow.cam.fieldOfView;
            val = Mathf.Clamp(val, minZoom, maxZoom);
            follow.cam.fieldOfView = val;
        }
    }

    bool CanMove()
    {
        if (isDeath) return false;
        return true;
    }

    public void Respawn()
    {
        transform.position = WorldManager.instance.respawnPoint.position;
        isDeath = false;
        hp = stats.maxHp;
        sync.IsDead(false);
        if(Photon.Pun.PhotonNetwork.IsConnected)
        {
            view.RPC("SyncronizeStat", Photon.Pun.RpcTarget.All, hp);
        }
    }
}

15パズル javascript

index.html

<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="utf-8">
  <title>15Puzzle</title>
  <style>
    canvas {
      background: pink;
      display: block;
      margin: 0 auto;
      cursor: pointer;
    }
  </style>
</head>

<body>
  <canvas width="280" height="280">
    Canvas not supported.
  </canvas>

  <script src="js/main.js"></script>
</body>

</html>

main.js

'use strict';

(() => {
    class PuzzleRenderer {
        constructor(puzzle, canvas) {
            this.puzzle = puzzle;
            this.canvas = canvas;
            this.ctx = this.canvas.getContext('2d');
            this.TILE_SIZE = 70;
            this.img = document.createElement('img');
            this.img.src = 'img/animal1.png';
            this.img.addEventListener('load', () => {
                this.render();
            });
            this.canvas.addEventListener('click', e => {
                if (this.puzzle.getCompletedStatus()) {
                    return;
                }

                const rect = this.canvas.getBoundingClientRect();
                const col = Math.floor((e.clientX - rect.left) / this.TILE_SIZE);
                const row = Math.floor((e.clientY - rect.top) / this.TILE_SIZE);
                this.puzzle.swapTiles(col, row);
                this.render();

                if (this.puzzle.isComplete()) {
                    this.puzzle.setCompletedStatus(true);
                    this.renderGameClear();
                }
            });
        }

        renderGameClear() {
            this.ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
            this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
            this.ctx.font = '28px Arial';
            this.ctx.fillStyle = '#fff';
            this.ctx.fillText('GAME CLEAR!!', 40, 150);
        }

        render() {
            for (let row = 0; row < this.puzzle.getBoardSize(); row++) {
                for (let col = 0; col < this.puzzle.getBoardSize(); col++) {
                    this.renderTile(this.puzzle.getTile(row, col), col, row);
                }
            }
        }

        renderTile(n, col, row) {
            if (n === this.puzzle.getBlankIndex()) {
                this.ctx.fillStyle = '#eeeeee';
                this.ctx.fillRect(
                    col * this.TILE_SIZE,
                    row * this.TILE_SIZE,
                    this.TILE_SIZE,
                    this.TILE_SIZE
                );
            } else {
                this.ctx.drawImage(
                    this.img,
                    (n % this.puzzle.getBoardSize()) * this.TILE_SIZE,
                    Math.floor(n / this.puzzle.getBoardSize()) * this.TILE_SIZE,
                    this.TILE_SIZE,
                    this.TILE_SIZE,
                    col * this.TILE_SIZE,
                    row * this.TILE_SIZE,
                    this.TILE_SIZE,
                    this.TILE_SIZE
                );
            }
        }
    }

    class Puzzle {
        constructor(level) {
            this.level = level;
            this.tiles = [
                [0, 1, 2, 3],
                [4, 5, 6, 7],
                [8, 9, 10, 11],
                [12, 13, 14, 15],
            ];
            this.UDLR = [
                [0, -1], // up
                [0, 1], // down
                [-1, 0], // left
                [1, 0], // right
            ];
            this.isCompleted = false;
            this.BOARD_SIZE = this.tiles.length;
            this.BLANK_INDEX = this.BOARD_SIZE ** 2 - 1;
            do {
                this.shuffle(this.level);
            } while (this.isComplete());
        }

        getBoardSize() {
            return this.BOARD_SIZE;
        }

        getBlankIndex() {
            return this.BLANK_INDEX;
        }

        getCompletedStatus() {
            return this.isCompleted;
        }

        setCompletedStatus(value) {
            this.isCompleted = value;
        }

        getTile(row, col) {
            return this.tiles[row][col];
        }

        shuffle(n) {
            let blankCol = this.BOARD_SIZE - 1;
            let blankRow = this.BOARD_SIZE - 1;

            for (let i = 0; i < n; i++) {
                let destCol;
                let destRow;
                do {
                    const dir = Math.floor(Math.random() * this.UDLR.length);
                    destCol = blankCol + this.UDLR[dir][0];
                    destRow = blankRow + this.UDLR[dir][1];
                } while (this.isOutside(destCol, destRow));

                [
                    this.tiles[blankRow][blankCol],
                    this.tiles[destRow][destCol],
                ] = [
                        this.tiles[destRow][destCol],
                        this.tiles[blankRow][blankCol],
                    ];

                [blankCol, blankRow] = [destCol, destRow];
            }
        }

        swapTiles(col, row) {
            if (this.tiles[row][col] === this.BLANK_INDEX) {
                return;
            }

            for (let i = 0; i < this.UDLR.length; i++) {
                const destCol = col + this.UDLR[i][0];
                const destRow = row + this.UDLR[i][1];

                if (this.isOutside(destCol, destRow)) {
                    continue;
                }

                if (this.tiles[destRow][destCol] === this.BLANK_INDEX) {
                    [
                        this.tiles[row][col],
                        this.tiles[destRow][destCol],
                    ] = [
                            this.tiles[destRow][destCol],
                            this.tiles[row][col],
                        ];
                    break;
                }
            }
        }

        isOutside(destCol, destRow) {
            return (
                destCol < 0 || destCol > this.BOARD_SIZE - 1 ||
                destRow < 0 || destRow > this.BOARD_SIZE - 1
            );
        }

        isComplete() {
            let i = 0;
            for (let row = 0; row < this.BOARD_SIZE; row++) {
                for (let col = 0; col < this.BOARD_SIZE; col++) {
                    if (this.tiles[row][col] !== i++) {
                        return false;
                    }
                }
            }
            return true;
        }
    }

    const canvas = document.querySelector('canvas');
    if (typeof canvas.getContext === 'undefined') {
        return;
    }

    new PuzzleRenderer(new Puzzle(2), canvas);
})();

.hack//VR 企画書

.hack//VR 企画書


【タイトル】

.hack//VR(ドットハック ヴィーアール)


【ジャンル】

フルダイブ型MMORPG(仮想現実大規模多人数同時接続型RPG)


【対応プラットフォーム】

  • Meta Quest 3 / Quest Pro
  • PlayStation VR2
  • PC VR(SteamVR対応)
  • フルダイブ対応ヘッドセット(将来的展望)

【コンセプト】

「仮想世界は、現実を超える」
架空のネットワークRPG《The World》を舞台にした、現実とリンクするフルダイブ型VR体験。ユーザーは「プレイヤー」としてゲームに参加しながらも、実はゲームの裏で進行する謎と陰謀に巻き込まれていく。


【開発背景】

  • 2002年の「.hack//Infection」シリーズから続く《The World》の世界観を最新技術で再構築。
  • フルダイブ技術の発展を見据えたVR MMORPGの実験的開発。
  • ユーザー間の社会的インタラクションとAI搭載NPCの自然会話を融合。

【ゲームの特徴】

1. フルダイブ没入体験

  • プレイヤーはアバターとして《The World》に降り立ち、五感(視覚、聴覚、触覚)を通じて世界を体験。
  • 拡張神経接続インターフェース「NeuroLink(仮)」対応。

2. 2層構造の物語

  • 仮想世界《The World》内のRPGストーリー。
  • 現実世界のプレイヤー間の謎、事件、そして「AI」の反乱。
  • 2つの時間軸が交錯し、プレイヤー自身が物語の一部になる。

3. AI NPCとのリアルな交流

  • GPT-4ベースの高度会話AI搭載NPC。
  • 各キャラクターが記憶・感情を持ち、ユーザーの選択により関係性が変化。

4. VR戦闘システム

  • ソードスキル、魔法、連携技をモーション操作でリアルタイム発動。
  • 攻撃タイミングと身体操作が勝敗を左右する、直感型バトル。

5. プレイヤーギルドとハッキング要素

  • ギルド運営、拠点の建築、PvPなどの要素に加え、
  • 特定プレイヤーが「ハッカー」となり、ゲームの奥深くに潜む謎の領域へアクセス可能。

【ゲーム内用語】

用語解説
The Worldゲームの舞台となる仮想空間
ネットスレイヤーデータを破壊する謎の存在
AIコア人格を持ったAI、暴走の兆しあり
データドレインプレイヤーの記憶や感情を吸収する技術(禁忌)

【ターゲット層】

  • VRゲーマー(16〜35歳)
  • SF・サイバーパンク・.hackシリーズファン
  • フルダイブ技術に興味を持つ先端層

【マネタイズ】

  • 基本無料+アバター装備・外見課金(スキン制)
  • ストーリーDLC、特定イベント有料開放
  • プレミアムアカウント(月額制)

【将来的展望】

  • プレイヤー間の記憶共有機能(記憶ログシェア)
  • 現実の感覚を記録・再体験する「リプレイVR」
  • 現実の時間と連動する仮想祭イベント

【イメージビジュアル】(必要なら生成可能)


【補足】

.hack//VRは「ゲーム」であると同時に、フルダイブVRの社会実験でもあります。AIと人間がどこまで共存できるか、仮想空間における「自我」とは何かを問いかける、哲学的な要素を含む作品です。

VRSNS

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>VR‑SNS Prototype v8 (Spatial Audio + PTT + エモート)</title>

<!-- Libraries -->
<script src="https://aframe.io/releases/1.5.0/aframe.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/aframe-extras@6.1.1/dist/aframe-extras.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gun/gun.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/simple-peer@9.11.1/simplepeer.min.js"></script>

<style>
html,body{margin:0;padding:0;overflow:hidden;font-family:"Helvetica Neue",Arial,sans-serif;background:#111;color:#eee}
#ui{position:fixed;top:0;left:0;width:420px;height:100vh;overflow-y:auto;background:rgba(0,0,0,.9);backdrop-filter:blur(6px);padding:1rem;box-sizing:border-box;z-index:10;transition:transform .3s ease}
#ui.hidden{transform:translateX(-105%)}
#ui h1{font-size:1.5rem;margin:0 0 .8rem;text-align:center}
#ui label{display:block;font-size:.75rem;margin:.6rem 0 .15rem;color:#8fc}
#ui input,#ui select,#ui textarea{width:100%;box-sizing:border-box;margin-bottom:.7rem;padding:.56rem;border:none;border-radius:6px;font-size:.9rem;background:#222;color:#ddd}
#ui textarea{resize:vertical}
#ui button{width:100%;padding:.72rem;border:none;border-radius:6px;font-weight:bold;cursor:pointer;background:#06a;color:#fff;transition:background .2s}
#ui button:hover{background:#089}
#timeline{margin-top:1rem}
.post{background:#1a1a1a;border-radius:6px;padding:.75rem;margin-bottom:.75rem;word-break:break-word;font-size:.8rem;position:relative}
.post small{color:#999;font-size:.7rem}
.post img{max-width:100%;border-radius:4px;margin-top:.4rem}
.reactBar{display:flex;gap:5px;margin-top:.45rem}
.reactBtn{flex:1;background:#333;border:none;border-radius:4px;padding:3px 0;font-size:.75rem;cursor:pointer;color:#f88;display:flex;justify-content:center;align-items:center}
.reactBtn span{margin-left:4px;font-weight:bold;color:#ccc}
.hashTag{color:#6af;cursor:pointer}
#handle{position:absolute;top:50%;right:-18px;width:18px;height:80px;border-radius:0 6px 6px 0;background:#06a;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#fff;font-size:.7rem;transform:translateY(-50%)}
#tagFilter{margin-bottom:.8rem;width:100%;padding:.56rem;border:none;border-radius:6px;background:#222;color:#ddd}
#onlineCount{font-size:.75rem;margin-bottom:.8rem;color:#9f9}
#roomInput{width:70%;display:inline-block}
#roomBtn{width:28%;display:inline-block;margin-left:2%}
#ttsToggleLbl,#muteToggleLbl,#pttInfo{display:flex;align-items:center;font-size:.75rem;margin-bottom:.7rem;gap:.5rem}
.sysMsg{color:#8ff;font-style:italic}
.speaking{animation:pulse 1s infinite}
@keyframes pulse{0%{opacity:1}50%{opacity:.4}100%{opacity:1}}
.emote{position:absolute;left:50%;transform:translateX(-50%);top:-0.4m;font-size:0.4m}
</style>
</head>
<body>
<div id="ui">
  <div id="handle">⮜</div>
  <h1>VR‑SNS</h1>

  <label>ルーム ID</label>
  <input id="roomInput" value="lobby"/><button id="roomBtn">入室</button>
  <div id="onlineCount">オンライン: 0</div>

  <label>ユーザー名</label>
  <input id="usernameInput" placeholder="匿名" />

  <label>アバター色</label>
  <input id="avatarColor" type="color" value="#ff8800" />

  <label>環境テーマ</label>
  <select id="themeSelect"><option value="dusk">夕暮れ空</option><option value="midnight">真夜中</option><option value="day">晴れ昼</option></select>

  <label>ハッシュタグ絞り込み</label>
  <select id="tagFilter"><option value="all">すべて</option></select>

  <label id="ttsToggleLbl"><input type="checkbox" id="ttsToggle"/> 投稿読み上げ</label>
  <label id="muteToggleLbl"><input type="checkbox" id="muteToggle"/> マイク常時OFF</label>
  <div id="pttInfo">Push‑To‑Talk: <strong>Vキー</strong> 押下中のみ送信</div>

  <label>投稿</label>
  <textarea id="postText" rows="3"></textarea>
  <button id="postButton">ポスト</button>
  <button id="clearButton" style="background:#a00;margin-top:.3rem">ローカル履歴削除</button>

  <div id="timeline"></div>
</div>

<a-scene renderer="antialias:true" xr-mode-ui="true" background="color:#112" cursor="rayOrigin:mouse">
  <a-entity environment="preset: forest; ground:y"></a-entity>
  <a-entity id="ground" geometry="primitive:plane; width:150; height:150" rotation="-90 0 0" material="visible:false" static-body></a-entity>

  <a-entity id="rig" position="0 1.6 4" movement-controls="fly:false; speed:0.22">
    <a-entity id="head" camera look-controls></a-entity>
    <a-entity id="leftHand"  laser-controls="hand:left"  raycaster="objects:.interactive" teleport-controls="button:trigger; collisionEntities:#ground; cameraRig:#rig"></a-entity>
    <a-entity id="rightHand" laser-controls="hand:right" raycaster="objects:.interactive"></a-entity>
  </a-entity>
</a-scene>

<script>
(()=>{
const gun=Gun({peers:['https://gun-manhattan.herokuapp.com/gun']});
const myId=Gun.text.random();
let room='lobby',roomRef;
const $=id=>document.getElementById(id);
const ui=$('ui'),handle=$('handle');
const roomInput=$('roomInput'),roomBtn=$('roomBtn'),onlineCount=$('onlineCount');
const usernameInput=$('usernameInput'),avatarColorIn=$('avatarColor');
const themeSelect=$('themeSelect'),tagFilter=$('tagFilter');
const ttsToggle=$('ttsToggle'),muteToggle=$('muteToggle');
const postText=$('postText'),postBtn=$('postButton'),clearBtn=$('clearButton');
const timelineEl=$('timeline');
const scene=document.querySelector('a-scene');
const head=$('head'),leftHand=$('leftHand'),rightHand=$('rightHand');

const store={get:(k,d)=>JSON.parse(localStorage.getItem('vrsns_'+k)||JSON.stringify(d)),set:(k,v)=>localStorage.setItem('vrsns_'+k,JSON.stringify(v))};
usernameInput.value=store.get('user','');avatarColorIn.value=store.get('color','#ff8800');themeSelect.value=store.get('theme','dusk');ttsToggle.checked=store.get('tts',false);

const postsMap=new Map();const tags=new Set();const remoteAvatars=new Map();const remotePeers=new Map();let micStream=null,aCtx=null;let pushTalking=false;
let presenceRef,posRef,presenceInterval,posInterval;
applyTheme(themeSelect.value);createOrUpdateAvatar();setupMic();enterRoom(room);

handle.onclick=()=>ui.classList.toggle('hidden');scene.addEventListener('enter-vr',()=>ui.classList.add('hidden'));scene.addEventListener('exit-vr',()=>ui.classList.remove('hidden'));
roomBtn.onclick=()=>{const r=roomInput.value.trim();if(r){leaveRoom();enterRoom(r)}};
themeSelect.onchange=e=>{const v=e.target.value;store.set('theme',v);applyTheme(v)};avatarColorIn.oninput=createOrUpdateAvatar;
ttsToggle.onchange=e=>store.set('tts',e.target.checked);
muteToggle.onchange=e=>updateMicState();
postBtn.onclick=submitPost;clearBtn.onclick=()=>{if(confirm('ローカル履歴を削除しますか?')){localStorage.removeItem('vrsns_posts_'+room);timelineEl.innerHTML='';}};
tagFilter.onchange=filterTimeline;
window.addEventListener('keydown',e=>{if(e.key==='v'&&!pushTalking){pushTalking=true;updateMicState();}});
window.addEventListener('keyup',e=>{if(e.key==='v'){pushTalking=false;updateMicState();}});

async function setupMic(){try{aCtx=new (window.AudioContext||window.webkitAudioContext)();micStream=await navigator.mediaDevices.getUserMedia({audio:true});updateMicState();}catch(err){alert('マイク利用不可');}}
function updateMicState(){if(!micStream)return;const enabled=!muteToggle.checked&&(pushTalking||muteToggle.checked===false&&ttsToggle);micStream.getAudioTracks()[0].enabled=enabled;}

function enterRoom(r){room=r;roomRef=gun.get('vrsns').get(room);roomInput.value=room;timelineEl.innerHTML='';postsMap.clear();tags.clear();rebuildTagFilter();onlineCount.textContent='オンライン: 0';
roomRef.get('posts').map().on(onPost);roomRef.get('posts').map().get('reactions').map().on(onReaction);
presenceRef=roomRef.get('presence').get(myId);presenceRef.put({ts:Gun.state(),color:avatarColorIn.value});presenceInterval=setInterval(()=>presenceRef.put({ts:Gun.state(),color:avatarColorIn.value}),10000);roomRef.get('presence').map().on(updateOnline);
posRef=roomRef.get('pos').get(myId);sendPos();posInterval=setInterval(sendPos,100);
roomRef.get('pos').map().on(onRemotePos);roomRef.get('signal').map().on(onSignal);sysMsg(`[${room}] 入室しました`);} 
function leaveRoom(){clearInterval(presenceInterval);clearInterval(posInterval);presenceRef&&presenceRef.put(null);posRef&&posRef.put(null);roomRef.off();roomRef.get('signal').off();remoteAvatars.forEach(av=>av.remove());remoteAvatars.clear();remotePeers.forEach(p=>p.destroy());remotePeers.clear();}

function submitPost(){const user=usernameInput.value.trim()||'匿名',text=postText.value.trim();if(!text)return;store.set('user',user);store.set('color',avatarColorIn.value);const id=Gun.text.random();const post={id,user,text,time:Date.now(),color:avatarColorIn.value,reactions:{'❤':0,'😂':0,'😮':0,'😢':0},tags:extractTags(text)};roomRef.get('posts').get(id).put(post);postText.value='';if(ttsToggle.checked)speak(`${user} さん: ${text.replace(/#/g,'')}`);} 
function onPost(post){if(!post||postsMap.has(post.id))return;postsMap.set(post.id,post);collectTags(post);rebuildTagFilter();renderPost2D(post);renderPost3D(post);filterTimeline();}
function onReaction(v,field,key){const pid=field,emoji=key;const p=postsMap.get(pid);if(!p)return;p.reactions[emoji]=v;updatePostCard(pid,p);updatePost3D(pid,p);} 
function react(id,emo){roomRef.get('posts').get(id).get('reactions').get(emo).once(v=>roomRef.get('posts').get(id).get('reactions').get(emo).put((v||0)+1));}

function updateOnline(){let c=0;roomRef.get('presence').map().once(()=>c++);setTimeout(()=>onlineCount.textContent=`オンライン: ${c}`,200);} 
function sendPos(){const p=head.object3D.position,l=leftHand.object3D.position,r=rightHand.object3D.position;posRef.put({x:p.x,y:p.y,z:p.z,l:{x:l.x,y:l.y,z:l.z},r:{x:r.x,y:r.y,z:r.z},color:avatarColorIn.value,ts:Gun.state()});}

function onRemotePos(data,id){if(id===myId||!data)return;let av=remoteAvatars.get(id);if(!av){av=createAvatar(id,data.color);remoteAvatars.set(id,av);connectVoice(id);}av.setAttribute('position',`${data.x} 0 ${data.z}`);av.querySelectorAll('[geometry]').forEach(g=>g.setAttribute('color',data.color||'#fff'));['l','r'].forEach(h=>{let el=av.querySelector('.'+h);if(!el){el=document.createElement('a-sphere');el.classList.add(h);el.setAttribute('radius',0.05);av.appendChild(el);}const hp=data[h];if(hp)el.setAttribute('position',`${hp.x} ${hp.y} ${hp.z}`);});}

function createAvatar(id,color){const root=document.createElement('a-entity');root.id='av_'+id;root.innerHTML=`<a-sphere radius='0.25' color='${color||'#fff'}' position='0 1.6 0'></a-sphere><a-cylinder height='0.8' radius='0.2' color='${color||'#fff'}' position='0 1 0'></a-cylinder>`;scene.appendChild(root);return root;}

function connectVoice(rid){if(remotePeers.has(rid)||!micStream)return;const init=myId>rid;const peer=new SimplePeer({initiator:init,trickle:true,stream:micStream});remotePeers.set(rid,peer);peer.on('signal',d=>roomRef.get('signal').get(myId).get(rid).put(JSON.stringify(d)));roomRef.get('signal').get(rid).get(myId).on(sig=>sig&&peer.signal(JSON.parse(sig)));peer.on('stream',s=>{const av=remoteAvatars.get(rid);if(!av)return;const a=new Audio();a.srcObject=s;const src=aCtx.createMediaStreamSource(s);const panner=aCtx.createPanner();panner.panningModel='HRTF';panner.distanceModel='inverse';panner.maxDistance=20;src.connect(panner).connect(aCtx.destination);function upd(){if(!av.parentNode)return;const pos=av.object3D.position;panner.setPosition(pos.x,pos.y,pos.z);requestAnimationFrame(upd);}upd();a.play();});peer.on('close',()=>remotePeers.delete(rid));peer.on('error',()=>remotePeers.delete(rid));}

function onSignal(){}

function renderPost2D(p){const isImg=/\.(gif|jpe?g|png|webp)$/i.test(p.text.trim());const card=document.createElement('div');card.className='post';card.dataset.id=p.id;card.dataset.tags=p.tags.join(',');card.innerHTML=`<strong style='color:${p.color}'>${escapeHTML(p.user)}</strong>: ${isImg?`<img src='${escapeHTML(p.text)}'/>`:linkify(escapeHTML(p.text))}${p.tags.map(t=>` <span class='hashTag'>#${t}</span>`).join('')}<br><small>${new Date(p.time).toLocaleString()}</small>`;const bar=document.createElement('div');bar.className='reactBar';['❤','😂','😮','😢'].forEach(e=>{const b=document.createElement('button');b.className='reactBtn';b.dataset.e=e;b.innerHTML=`${e} <span>${p.reactions[e]||0}</span>`;b.onclick=()=>react(p.id,e);bar.appendChild(b);});card.appendChild(bar);card.querySelectorAll('.hashTag').forEach(el=>el.onclick=()=>{tagFilter.value=el.textContent.slice(1).toLowerCase();filterTimeline();});timelineEl.insertBefore(card,timelineEl.firstChild);} 
function updatePostCard(id,p){const card=document.querySelector(`.post[data-id='${id}']`);if(card)card.querySelectorAll('.reactBtn').forEach(btn=>btn.querySelector('span').textContent=p.reactions[btn.dataset.e]||0);} 
function format3D(p){const top=Object.entries(p.reactions||{}).sort((a,b)=>b[1]-a[1])[0];const r=top&&top[1]>0?` ${top[0]}×${top[1]}`:'';return `${p.user}${r}\n${p.text}`;}
function renderPost3D(p){const ent=document.createElement('a-entity');ent.classList.add('interactive');ent.id='p3d_'+p.id;ent.setAttribute('text',{value:format3D(p),align:'center',width:4,color:p.color});ent.onclick=()=>react(p.id,'❤');positionEntity(ent);scene.appendChild(ent);setTimeout(()=>ent.remove(),300000);if(/\.(gif|jpe?g|png|webp)$/i.test(p.text.trim())){const plane=document.createElement('a-plane');plane.setAttribute('src',p.text);plane.setAttribute('width',1.8);plane.setAttribute('height',1.2);plane.classList.add('interactive');positionEntity(plane,0.6);scene.appendChild(plane);setTimeout(()=>plane.remove(),300000);}}
function updatePost3D(id,p){const e=$('p3d_'+id);if(e)e.setAttribute('text','value',format3D(p));}

function positionEntity(e,y=0){const ang=Math.random()*Math.PI,rad=5+Math.random()*5;e.setAttribute('position',`${Math.cos(ang)*rad} ${2+y} ${-Math.sin(ang)*rad}`);} 
function collectTags(p){p.tags.forEach(t=>tags.add(t.toLowerCase()));}
function rebuildTagFilter(){const cur=tagFilter.value;tagFilter.innerHTML='<option value="all">すべて</option>'+[...tags].sort().map(t=>`<option value='${t}'>#${t}</option>`).join('');tagFilter.value=cur||'all';}
function filterTimeline(){const f=tagFilter.value;document.querySelectorAll('.post').forEach(el=>el.style.display=(f==='all'||el.dataset.tags.includes(f))?'':'none');}
function escapeHTML(s){return s.replace(/[&<>"']/g,ch=>({'&':'&amp;','<':'&lt;',">":"&gt;","\"":"&quot;","'":"&#39;"}[ch]));}
function extractTags(t){return(t.match(/#(\w+)/g)||[]).map(x=>x.slice(1).toLowerCase());}
function linkify(t){return t.replace(/https?:\/\/\S+/g,u=>`<a href='${u}' target='_blank'>${u}</a>`);} 
function applyTheme(t){let sky=scene.querySelector('a-sky');if(!sky){sky=document.createElement('a-sky');scene.appendChild(sky);}sky.setAttribute('color',t==='midnight'?'#000022':t==='day'?'#86cefa':'#112');}
function createOrUpdateAvatar(){const c=avatarColorIn.value;store.set('color',c);let av=$('myAvatar');const body=`<a-sphere radius='0.25' color='${c}' position='0 1.6 0'></a-sphere><a-cylinder height='0.8' radius='0.2' color='${c}' position='0 1 0'></a-cylinder>`;if(!av){av=document.createElement('a-entity');av.id='myAvatar';av.innerHTML=body;scene.appendChild(av);}else av.innerHTML=body;}
function speak(msg){speechSynthesis.speak(new SpeechSynthesisUtterance(msg));}
function sysMsg(t){const el=document.createElement('div');el.className='post sysMsg';el.textContent=t;timelineEl.insertBefore(el,timelineEl.firstChild);} 
})();
</script>
</body>
</html>