集めた仲間によって評価が変わるRPGの作成 ~NPC配置編~

 集めた仲間によって評価が変わるRPGの作成
~NPC配置編~


こんにちは、東京経済大学3年のSTです。

今回は、前回作成した、マップに各NPCを設置していきたいと思います。

マップ用のコードは前回説明したのでここでは省きます。

コード内に会話用コードも仕込まれていますが今回は使用しません。

次回からのシナリオ構成などから使用します。


1,マップの説明

マップ0

マップ1

マップ2

マップ3

マップ4

マップ5

マップ6

マップ7

マップ8

マップ9

マップ10

マップ11



2,コードの説明

   // =========================
    //  NPC画像プリロード(JSで生成)
    // =========================
    const NPC_SRC = {
      granny1: "おばあさん1.png",
      granny2: "おばあさん2.png",
      oldman1: "おじいさん1.png",
      oldman2: "おじいさん2.png",
      maid1: "メイド1.png",
      maid2: "メイド2.png",
      guard1: "衛兵1.png",
      guard2: "衛兵2.png",
      guard3: "衛兵3.png",
      guard4: "衛兵4.png",
      king: "王様.png",
      fighter1: "格闘家1.png",
      fighter2: "格闘家2.png",
      fighter3: "格闘家3.png",
      researcher: "研究家1.png",
      dog1: "犬1.png",
      dog2: "犬2.png",
      samurai1: "侍1.png",
      samurai2: "侍2.png",
      warrior1: "戦士1.png",
      warrior2: "戦士2.png",
      warrior3: "戦士3.png",
      warrior4: "戦士4.png",
      warrior5: "戦士5.png",
      monk1: "僧侶1.png",
      monk2: "僧侶2.png",
      monk3: "僧侶3.png",
      aide1: "側近1.png",
      aide2: "側近2.png",
      vill1: "村人1.png",
      vill2: "村人2.png",
      vill3: "村人3.png",
      vill4: "村人4.png",
      vill5: "村人5.png",
      vill6: "村人6.png",
      rogue1: "盗賊1.png",
      rogue2: "盗賊2.png",
      cat1: "猫1.png",
      cat2: "猫2.png",
      mage1: "魔法使い1.png",
      mage2: "魔法使い2.png",
      mage3: "魔法使い3.png",
      mage4: "魔法使い4.png",
      skeletonKing: "骸骨キング.png",
      skeletonKnight2: "骸骨騎士2.png",
      darkMage: "闇の魔術師.png",
      seaWarrior: "海の戦士.png",
      pumpkinMan: "パンプキンマン.png",
      skeletonSwordsman: "骸骨剣士.png",
      dungeonBoss: "ダンジョンボス.png",
      darkLord: "闇の王.png",
      warrior6: "戦士6.png",
      ghost: "ゴースト.png",
      warrior7: "戦士7.png",
      warrior8: "戦士8.png",
      warrior9: "戦士9.png",
    };
    const imgNPC = {};
    (function preloadNPCs(){
      Object.entries(NPC_SRC).forEach(([key,src])=>{
        const im = new Image();
        im.src = src;
        imgNPC[key] = im;
      });
    })();

役割
NPCの見た目ID → 画像ファイル名と、画像の事前読み込み。
sprite には 左のキー名(例:"darkLord")を使い、実ファイル名は辞書の値と一致させる。
画像は <img> タグではなく JSで読み込みnew Image())。
追加時は NPC_SRC に1行増やすだけでOK。

今後の開発的に、
一般NPCでおじいさん、おばあさん、メイド、衛兵、村人など
仲間になるNPCとして、戦士、格闘家、魔法使い、僧侶など
一般NPCとの会話やフラグ建築などによって仲間になるようにしていきたいと思います。





// =========================
    //  NPC配置・会話(全員分スロット)
    //   - active:true  → 出現&会話可
    //   - active:false → 非表示(配置前)
    //   - stage: 0..11 / x:0..19 / y:0..10
    // =========================
    const npcs = [
      // --- ここから配置済み ---
      

      {active:true,  name:"戦士",       sprite:"warrior3",stage:0,  x:14, y:6,  solid:true,  talk:"武器は心だ。剣はその延長にすぎない。"},
      {active:true,  name:"魔法使い",   sprite:"mage4",   stage:0,  x: 1, y:6,  solid:true,  talk:"湖面の揺らぎは、別の世界の反射かもしれぬ。"},     
      {active:true,  name:"衛兵",       sprite:"guard2",  stage:0,  x:18, y:9,  solid:true,  talk:"城内は静粛に。王に無礼のないようにな。"},
      {active:true,  name:"衛兵",       sprite:"guard3",  stage:0,  x:17, y:9,  solid:true,  talk:"城内は静粛に。王に無礼のないようにな。"},
      {active:true,  name:"衛兵",       sprite:"guard2",  stage:0,  x:8, y:9,  solid:true,  talk:"城内は静粛に。王に無礼のないようにな。"},
   {active:true,  name:"衛兵",       sprite:"guard3",  stage:0,  x:7, y:9,  solid:true,  talk:"城内は静粛に。王に無礼のないようにな。"},
      {active:true, name:"衛兵",       sprite:"guard2",    stage:0,  x: 3, y:2,  solid:true,  talk:"ここは立入禁止だ。"},
      {active:true, name:"衛兵",       sprite:"guard3",    stage:0,  x: 2, y:2,  solid:true,  talk:"ここは立入禁止だ。"},
      {active:true, name:"衛兵",       sprite:"guard1",    stage:0,  x: 3, y:6,  solid:true,  talk:"ここは立入禁止だ。"},
      {active:true, name:"衛兵",       sprite:"guard1",    stage:0,  x: 2, y:6,  solid:true,  talk:"ここは立入禁止だ。"},
      {active:true, name:"衛兵",       sprite:"guard4",    stage:0,  x: 9, y:2,  solid:true,  talk:"不審者はいないか?"},
      {active:true, name:"衛兵",       sprite:"guard4",    stage:0,  x: 8, y:2,  solid:true,  talk:"不審者はいないか?"},
      {active:true, name:"衛兵",       sprite:"guard4",    stage:0,  x: 10, y:2,  solid:true,  talk:"不審者はいないか?"},
      {active:true,  name:"衛兵",       sprite:"guard3",  stage:1,  x:8, y:7,  solid:true,  talk:"城内は静粛に。王に無礼のないようにな。"},
      {active:true,  name:"衛兵",       sprite:"guard3",  stage:1,  x:8, y:8,  solid:true,  talk:"城内は静粛に。王に無礼のないようにな。"},
   {active:true,  name:"衛兵",       sprite:"guard3",  stage:1,  x:8, y:9,  solid:true,  talk:"城内は静粛に。王に無礼のないようにな。"},
      {active:true, name:"衛兵",       sprite:"guard2",    stage:1,  x: 12, y:7,  solid:true,  talk:"巡回中だ、通行人よ。"},
      {active:true, name:"衛兵",       sprite:"guard2",    stage:1,  x: 12, y:8,  solid:true,  talk:"巡回中だ、通行人よ。"},
      {active:true, name:"衛兵",       sprite:"guard2",    stage:1,  x: 12, y:9,  solid:true,  talk:"巡回中だ、通行人よ。"},
      {active:true, name:"衛兵",       sprite:"guard1",    stage:1,  x: 1, y:4,  solid:true,  talk:"巡回中だ、通行人よ。"},
      {active:true, name:"衛兵",       sprite:"guard1",    stage:1,  x: 2, y:4,  solid:true,  talk:"巡回中だ、通行人よ。"},
      {active:true, name:"衛兵",       sprite:"guard1",    stage:1,  x: 3, y:4,  solid:true,  talk:"巡回中だ、通行人よ。"},
      {active:true, name:"衛兵",       sprite:"guard1",    stage:1,  x: 4, y:4,  solid:true,  talk:"巡回中だ、通行人よ。"},
      {active:true, name:"衛兵",       sprite:"guard1",    stage:1,  x: 5, y:4,  solid:true,  talk:"巡回中だ、通行人よ。"},
      {active:true, name:"衛兵",       sprite:"guard1",    stage:1,  x: 6, y:4,  solid:true,  talk:"巡回中だ、通行人よ。"},
      {active:true, name:"衛兵",       sprite:"guard1",    stage:1,  x: 13, y:4,  solid:true,  talk:"巡回中だ、通行人よ。"},
      {active:true, name:"衛兵",       sprite:"guard1",    stage:1,  x: 14, y:4,  solid:true,  talk:"巡回中だ、通行人よ。"},
      {active:true, name:"衛兵",       sprite:"guard1",    stage:1,  x: 15, y:4,  solid:true,  talk:"巡回中だ、通行人よ。"},
      {active:true, name:"衛兵",       sprite:"guard1",    stage:1,  x: 16, y:4,  solid:true,  talk:"巡回中だ、通行人よ。"},
      {active:true, name:"衛兵",       sprite:"guard1",    stage:1,  x: 17, y:4,  solid:true,  talk:"巡回中だ、通行人よ。"},
      {active:true, name:"衛兵",       sprite:"guard1",    stage:1,  x: 18, y:4,  solid:true,  talk:"巡回中だ、通行人よ。"},
      {active:true,  name:"王様",       sprite:"king",    stage:1,  x:10, y:4,  solid:true,  talk:"よく来た、勇者よ。海の向こうに\n不穏な気配が漂っておる。"},
      {active:true,  name:"側近",       sprite:"aide1",   stage:1,  x:11, y:4,  solid:true,  talk:"陛下はそなたに期待しておられる。"},
      {active:true,  name:"側近",       sprite:"aide2",   stage:1,  x:9, y:4,  solid:true,  talk:"陛下はそなたに期待しておられる。"},
      {active:true, name:"戦士",       sprite:"warrior1",  stage:1,  x: 1, y:2,  solid:true,  talk:"盾は家族、剣は仲間。"},
      {active:true, name:"戦士",       sprite:"warrior2",  stage:1,  x: 18, y:2,  solid:true,  talk:"背中は任せろ。"},
      {active:true,  name:"侍",         sprite:"samurai1",stage:1,  x:6, y:9,  solid:true,  talk:"道は一つにあらず。だが迷ったら、\n己が信じる方へ進め。"},
      {active:true, name:"戦士",       sprite:"warrior4",  stage:2,  x: 6, y:3,  solid:true,  talk:"恐れは一歩目で消える。"},
      {active:true, name:"戦士",       sprite:"warrior5",  stage:2,  x: 17, y:10,  solid:true,  talk:"鍛錬は裏切らない。"},
      {active:true, name:"侍",         sprite:"samurai2",  stage:2,  x:1, y:3,  solid:true,  talk:"一刀両断。迷いの根を断て。"},
      {active:true, name:"僧侶",       sprite:"monk1",     stage:2,  x: 0, y:7,  solid:true,  talk:"静寂の中に道がある。"},
      {active:true, name:"戦士6",       sprite:"warrior6",    stage:2,  x: 4, y:7,  solid:true,  talk:"巡回中だ、通行人よ。"},
      {active:true, name:"ダンジョンボス",       sprite:"dungeonBoss",    stage:3,  x: 5, y:4,  solid:true,  talk:"巡回中だ、通行人よ。"},
      {active:true, name:"骸骨王",       sprite:"skeletonKing",    stage:3,  x: 8, y:9,  solid:true,  talk:"巡回中だ、通行人よ。"},
      {active:true, name:"闇の王",       sprite:"darkLord",    stage:3,  x: 14, y:4,  solid:true,  talk:"巡回中だ、通行人よ。"},
      {active:true,  name:"おばあさん", sprite:"granny1", stage:4,  x:14, y:4,  solid:true,  talk:"ようこそ。ここは港町だよ。\n北の洞窟には近づかない方がいいねぇ。"},
      {active:true,  name:"おじいさん", sprite:"oldman2", stage:4,  x:12, y:4,  solid:true,  talk:"むかし、この湖で\n伝説の船を見た者がいたそうじゃ。"},
      {active:true, name:"おばあさん", sprite:"granny2",   stage:4,  x: 3, y:1,  solid:true,  talk:"若いの、体を大事にするんだよ。"},
      {active:true, name:"おじいさん", sprite:"oldman1",   stage:4,  x: 2, y:8,  solid:true,  talk:"北の風は変わりやすい。海に出るときは注意じゃ。"},
      {active:true, name:"メイド",     sprite:"maid1",     stage:4,  x: 12, y:1,  solid:true,  talk:"今日は王の間の大掃除です。"},
      {active:true,  name:"メイド",     sprite:"maid2",   stage:4,  x:13, y:1,  solid:true,  talk:"お掃除の途中なんです。足元に気をつけて!"},
      {active:true,  name:"犬",         sprite:"dog2",    stage:4, x: 8, y:8,  solid:true, talk:"ワン!"},
      {active:true,  name:"猫",         sprite:"cat1",    stage:4, x: 4, y:4,  solid:true, talk:"ニャー。"},
      {active:true,  name:"僧侶",       sprite:"monk2",   stage:4,  x: 18, y:8,  solid:true,  talk:"祈りは道を照らします。焦らず進みなさい。"},
      {active:true, name:"僧侶",       sprite:"monk3",     stage:4,  x: 1, y:4,  solid:true,  talk:"光はいつも近くに。"},
      {active:true,  name:"衛兵",       sprite:"guard3",  stage:4,  x:8, y:4,  solid:true,  talk:"城内は静粛に。王に無礼のないようにな。"},
      {active:true,  name:"衛兵",       sprite:"guard2",  stage:4,  x:12, y:8,  solid:true,  talk:"城内は静粛に。王に無礼のないようにな。"},
      {active:true, name:"村人",       sprite:"vill1",     stage:5,  x: 4, y:4,  solid:true,  talk:"魚がよく獲れる季節だよ。"},
      {active:true, name:"村人",       sprite:"vill2",     stage:5,  x: 5, y:4,  solid:true,  talk:"最近、夜の湖が光るんだ。"},
      {active:true, name:"村人",       sprite:"vill6",     stage:5,  x: 9, y:1,  solid:true,  talk:"洞窟から不思議な音がする…"},
      {active:true,  name:"研究家",     sprite:"researcher",stage:5,x: 12, y:3,  solid:true,  talk:"鉱山の奥で、奇妙な反応を観測した…!"},
      {active:true, name:"研究家",     sprite:"researcher",stage:5,  x:13, y:3,  solid:true,  talk:"データが示す…ここには秘密がある。"},
      {active:true, name:"魔法使い",   sprite:"mage2",     stage:5,  x:17, y:2,  solid:true,  talk:"杖は道しるべ。"},
      {active:true, name:"魔法使い",   sprite:"mage3",     stage:5,  x:1, y:2,  solid:true,  talk:"心を静めれば見えるものがある。"},
      {active:true,  name:"格闘家",     sprite:"fighter2",stage:6,  x: 1, y:9,  solid:true,  talk:"足運びがすべてだ。まずは四方の練習からだ。"},
      {active:true, name:"格闘家",     sprite:"fighter1",  stage:6,  x: 18, y:1,  solid:true,  talk:"呼吸を整えろ。拳は心の律動だ。"},
      {active:true, name:"格闘家",     sprite:"fighter3",  stage:6,  x: 11, y:5,  solid:true,  talk:"無駄な力を抜け。"},
      {active:true, name:"闇の魔術師",       sprite:"darkMage",    stage:6,  x: 18, y:9,  solid:true,  talk:"巡回中だ、通行人よ。"},
      {active:true, name:"パンプキンマン",       sprite:"pumpkinMan",    stage:6,  x:5 , y:6,  solid:true,  talk:"巡回中だ、通行人よ。"},
      {active:true, name:"骸骨騎士",       sprite:"skeletonKnight2",    stage:7,  x: 0, y:1,  solid:true,  talk:"巡回中だ、通行人よ。"},
      {active:true, name:"骸骨剣士",       sprite:"skeletonSwordsman",    stage:7,  x: 19, y:4,  solid:true,  talk:"巡回中だ、通行人よ。"},
      {active:true, name:"ゴースト",       sprite:"ghost",    stage:7,  x: 19, y:6,  solid:true,  talk:"巡回中だ、通行人よ。"},
      {active:true, name:"村人",       sprite:"vill4",     stage:8,  x: 7, y:1,  solid:true,  talk:"旅人さん、宿なら北のテントへ。"},
      {active:true, name:"村人",       sprite:"vill5",     stage:8,  x: 8, y:1,  solid:true,  talk:"見張りの丸太を壊すなよ。"},
      {active:true, name:"犬",         sprite:"dog1",      stage:8, x: 17, y:8,  solid:true, talk:"ワフッ!"},
      {active:true, name:"猫",         sprite:"cat2",      stage:8, x: 4, y:4,  solid:true, talk:"フニャ。"},
      {active:true,  name:"盗賊",       sprite:"rogue1",  stage:8,  x:1, y:8,  solid:true,  talk:"…行き止まりが多いって? 目を凝らせ。"},
      {active:true, name:"盗賊",       sprite:"rogue2",    stage:8,  x:12, y:4,  solid:true,  talk:"道は一つとは限らないぜ。"},
      {active:true, name:"戦士8",       sprite:"warrior8",    stage:8,  x: 1, y:1,  solid:true,  talk:"巡回中だ、通行人よ。"},
   {active:true, name:"戦士9",   sprite:"warrior9",     stage:8,  x: 6, y:8,  solid:true,  talk:"波紋に問い、風に聞け。"},
      {active:true,  name:"村人",       sprite:"vill3",   stage:9,  x: 0, y:0,  solid:true,  talk:"鉱山のレールは古い。足を滑らすなよ。"},
      {active:true, name:"海の戦士",       sprite:"seaWarrior",    stage:9,  x: 0, y:10,  solid:true,  talk:"巡回中だ、通行人よ。"},       
      {active:true, name:"魔法使い",   sprite:"mage1",     stage:11,  x: 9, y:2,  solid:true,  talk:"波紋に問い、風に聞け。"},
      {active:true, name:"戦士7",       sprite:"warrior7",    stage:11,  x: 17, y:5,  solid:true,  talk:"巡回中だ、通行人よ。"},      
    ];

役割:全NPCの状態・場所・会話テキストを1体1行で定義。
各プロパティ

  • activetrue で出現。false で非表示(将来のイベント連動用に便利)。

  • name:会話ウィンドウのタイトル(表示名)。

  • sprite:見た目。NPC_SRCのキーを指定。

  • stage:出現するステージ番号(0〜11)。

  • x, y:20×11グリッドでの位置(整数)。

  • solidtrue で通れない当たり判定。false ならすり抜け可能

  • talk:会話本文。\n で改行可。





    // シンプルメッセージ
    let msgBox = null;
    function showMsg(title, text){ msgBox = {title, text}; fieldpaint(); }
    function hideMsg(){ msgBox = null; fieldpaint(); }

    // 会話キー(X/Enter)追加
    const _onKeyBase = onKey;
    onKey = function(e){
      const code = e.keyCode;
      if(msgBox && (code===88 || code===13)){ hideMsg(); return; } // 表示中は閉じる

      if(code===88 || code===13){ // 会話試行
        const {x:fx,y:fy} = frontOfPlayer();
        if(inBounds(stage,fx,fy)){
          const n = npcAt(stage,fx,fy);
          if(n){ showMsg(n.name, n.talk); return; }
        }
      }


役割:会話開始/終了の状態管理

表示中は msgBox にオブジェクト、閉じたら null

fieldpaint() がUI描画を担当。

X(88)/Enter(13) を押したら前方にNPCがいるか確認 → いたら会話開始

会話表示中に同キーで閉じる。

元の onKey をラップして移動処理を壊さない構造。


3,動作について

全体の流れ

ページが開くと init() が実行
canvas を取得→ドット絵補間OFF
すべてのタイル画像(森・床・壁・海・船など)を <img> から参照
NPCスプライトは NPC_SRC をもとに JavaScript側でプリロード
keydown を監視して、初回の画面描画 fieldpaint() を実行
画面描画 fieldpaint()(毎フレームではなく、操作があった時に描く)
①タイル → ②NPC → ③プレイヤー(or 船) → ④会話ウィンドウ
の順でレイヤー表示
マップは 1 つの「ステージ」(20×11タイル)。ステージは 4×3 のグリッドCOLS=4, ROWS=3)で並んでいる

キー操作
←↑→↓:移動(1 マスずつ)
Z:船の乗り降り(乗れる/降りられる場所の条件あり)
X または Enter:NPCに話しかける/会話ウィンドウを閉じる
1 マス移動の裏側(キー押下→反映まで)
onKey(e) が呼ばれ、方向キーなら候補座標 (dx, dy) を作る
その座標が画面内なら canStep(stage, dx, dy) で進めるか判定
タイルが通行不可(blocked)ならNG
そのマスに solid:true のNPC がいればNG
船に乗っているかどうかで「踏めるタイル集合」が切り替わる
陸歩行:landWalk(床/石床/丸太橋/洞窟床など)
船移動:seaTiles(暗い海の2種)+一部渡し用タイル(丸太/船)
進めるなら (px, py) を更新 → afterMove() を呼ぶ
船に乗っている状態で陸タイルに到達したら 自動で下船 する挙動あり
fieldpaint() を呼んで画面更新
画面の端に到達したら(ステージ遷移)
端の外へ出ようとすると edgeDirection() が向きを返す
neighborIndex(stage, dir)隣のステージ番号を計算
例えば右端から出ると、次ステージの x=0 に入り直す(y は維持)
次ステージでも canStep を再チェックしてから移る

船の乗り降り(Z キー)
乗船条件:プレイヤーの足元が boatTiles(船/渡し台タイル)
下船条件:プレイヤーの足元が landWalk(陸で降りられる)
乗船中は 海系タイルのみを進める(丸太橋など一部を除く)
方向は boatDir に記録(プレイヤー/船の表示や、前方1マスの取得に使用)
NPCとの会話(X / Enter)
X または Enter を押すと、プレイヤーの向きの一歩前frontOfPlayer() で取得
その座標に npcAt(stage, fx, fy)(同ステージ・同座標・active!==false の NPC)がいれば会話開始
会話は msgBox に格納され、画面右下にメッセージウィンドウが出る
会話表示中に X/Enter を押すと閉じる(移動は一旦止まる)
当たり判定の考え方
タイル衝突blocked に列挙されたIDは通れない(森・壁・店・柱・洞窟壁など)
NPC衝突:同マスに solid:true の NPC がいれば通れない
船ON/OFFで通行可能タイルセットを切り替え(陸と海の世界ルールを分離)
画面描画の順序(見た目が破綻しない理由)
タイル(地形・装飾)
NPC(背景より上に立つ)
プレイヤー/船(NPCより前)
会話ウィンドウ(最前面、半透明黒+白枠+文字)
→ レイヤーの優先順が固定なので、重なり関係が常に安定
ステージ配列と位置づけ
ステージは map = [map0,map1,map2,map9, map3,map4,map5,map10, map6,map7,map8,map11]
4 列 × 3 行の並び順(左上= index 0)。neighborIndex() がこの 格子に沿って上下左右を求め、エッジ遷移に使う
まとめ(プレイ感)
「矢印で歩く/Zで船」「X/Enterで会話」だけのシンプル操作
陸と海のルールがハッキリしていて、進路計画に小さなパズル性

4,参考文献・引用サイト

ぴぽや倉庫
ドット絵世界
DOT  ILLOST
効果音ラボ