7 Commits 9518cab740 ... 5ccaffe4da

Author SHA1 Message Date
  caryoscelus 5ccaffe4da DJII: more char creation keyboard nav 10 months ago
  caryoscelus 7d89f6154f DJII: char creation improve keyboard navigation 10 months ago
  caryoscelus 8ccec4d55d DJII: rewrite profession select, now with style & keyboard control 10 months ago
  caryoscelus 25b8e91532 DJII: customize player name 10 months ago
  caryoscelus 1025bad03a Stat editing with keyboard 10 months ago
  caryoscelus 0b6b58ed8b DJII: Place stairwells on office floors 10 months ago
  caryoscelus d0fc01a3ef DJII: spawn another floor 10 months ago

+ 47 - 0
src/routes/Select.svelte

@@ -0,0 +1,47 @@
+<script>
+  let { options, value } = $props();
+
+  const onkeydown = (ev) => {
+    // TODO: support custom controls
+    const ix = options.findIndex(({name}) => name === value.name);
+    if (ev.key === 'ArrowUp') {
+      const newIx = Math.max(0, ix-1);
+      value = options[newIx];
+    } else if (ev.key === 'ArrowDown') {
+      const newIx = Math.min(options.length-1, ix+1);
+      value = options[newIx];
+    }
+  };
+
+  const selectedClass = (option) => {
+    if (option.name === value.name) {
+      return 'selectable-selected';
+    }
+    return '';
+  };
+
+  const select = (option) => {
+    value = option;
+  };
+</script>
+
+<ul tabindex="0" {onkeydown}>
+  {#each options as option}
+    <li class="selectable {selectedClass(option)}" onclick={() => select(option)}>
+      {option.name}
+    </li>
+  {/each}
+</ul>
+
+<style>
+  .selectable {
+  }
+
+  .selectable:hover {
+    background-color: #999;
+  }
+
+  .selectable-selected {
+    background-color: #bbb;
+  }
+</style>

+ 20 - 23
src/routes/djii/CharCreation.svelte

@@ -1,38 +1,44 @@
 <script>
   import { Name } from '$lib/components';
   import { addComponent } from '$lib/ecs';
-  import Stat from './Stat.svelte';
+  import { onMount } from 'svelte';
+  import Select from '../Select.svelte';
   import { newGame } from './game';
   import { enumProfessions } from './profession';
   import { enumSkills } from './skill';
-  import { FreeStats, enumStats } from './stat';
+  import Stats from './Stats.svelte';
 
   let { game } = $props();
 
   const game0 = newGame();
 
+  let charNameWidget
   let tick = $state(false);
   
-  let freePoints = $derived((tick, FreeStats.amount[game0.pc]));
   let error = $state('');
 
   let playerName = $state('I.I.');
 
-  const stats = enumStats();
   const professions = enumProfessions();
   const skills = enumSkills();
 
+  let selectedProfession = $state(professions[0]);
+
   // skip char-creation (for debug purposes)
-  const autostart = true;
+  const autostart = false;
 
-  $effect(() => {
-    tick;
-    error = '';
+  onMount(() => {
+    charNameWidget.focus();
     if (autostart) {
       start();
     }
   });
 
+  $effect(() => {
+    tick;
+    error = '';
+  });
+
   const start = () => {
     addComponent(game0.world, Name, game0.pc, {
       name: playerName,
@@ -41,36 +47,27 @@
   };
 
   const finishCharacter = () => {
+    /*
     if (freePoints > 0) {
       error = 'Free stat points left!';
       return;
     }
-    game = game0;
+    */
+    start();
   };
 </script>
 
 <section>
   <h2>New character</h2>
+  <input bind:this={charNameWidget} bind:value={playerName} /> the {selectedProfession.name}
   <div>
     <h3>Profession</h3>
-    <select size={professions.length}>
-      {#each professions as [name, prof]}
-        <option >
-          {name}
-        </option>
-      {/each}
-    </select>
+    <Select options={professions} bind:value={selectedProfession} />
   </div>
   <div>
     <h3>Hobby</h3>
   </div>
-  <div>
-    <h3>Stats</h3>
-    {#each stats as [_, stat]}
-      <Stat {stat} character={game0.pc} bind:tick />
-    {/each}
-    {freePoints} left
-  </div>
+  <Stats game={game0} bind:tick />
   <div>
     <h3>Skills</h3>
     {#each skills as [name, skill]}

+ 13 - 3
src/routes/djii/Stat.svelte

@@ -22,10 +22,20 @@
       tick = !tick;
     }
   };
+
+  const onkeydown = (ev) => {
+    // TODO: support common custom arrow bindings
+    if (ev.key === '+' || ev.key === 'ArrowRight') {
+      plus();
+    } else if (ev.key === '-' || ev.key === 'ArrowLeft') {
+      minus();
+    }
+    console.log(ev.key);
+  };
 </script>
 
-<div>
+<div tabindex="0" {onkeydown}>
   {componentName(stat)} {amount}
-  <button onclick={minus}>-</button>
-  <button onclick={plus}>+</button>
+  <button tabindex="-1" onclick={minus}>-</button>
+  <button tabindex="-1" onclick={plus}>+</button>
 </div>

+ 57 - 0
src/routes/djii/Stats.svelte

@@ -0,0 +1,57 @@
+<script>
+  import Stat from "./Stat.svelte";
+  import { FreeStats, enumStats } from "./stat";
+
+  const stats = enumStats();
+
+  let { tick, game } = $props();
+
+  let innerWidget;
+
+  let freePoints = $derived((tick, FreeStats.amount[game.pc]));
+
+  const onkeydown = (ev) => {
+    // TODO: maybe there's a better way to change focus?
+    if (ev.key === 'ArrowUp') {
+      for (let child of innerWidget.children) {
+        if (document.activeElement === child) {
+          let el = child;
+          // we have to skip comment elements inserted by svelte
+          while (el = el.previousSibling) {
+            // UGH, this depends on Stat being div
+            if (el.nodeName.toLowerCase() === 'div') {
+              el.focus();
+              break;
+            }
+          }
+          break;
+        }
+      }
+    } else if (ev.key === 'ArrowDown') {
+      for (let child of innerWidget.children) {
+        if (document.activeElement === child) {
+          let el = child;
+          // we have to skip comment elements inserted by svelte
+          while (el = el.nextSibling) {
+            // UGH, this depends on Stat being div
+            if (el.nodeName.toLowerCase() === 'div') {
+              el.focus();
+              break;
+            }
+          }
+          break;
+        }
+      }
+    }
+  };
+</script>
+
+<div {onkeydown}>
+  <h3>Stats</h3>
+  <div bind:this={innerWidget}>
+    {#each stats as [_, stat]}
+      <Stat {stat} character={game.pc} bind:tick />
+    {/each}
+  </div>
+  {freePoints} left
+</div>

+ 21 - 8
src/routes/djii/intro.ts

@@ -6,7 +6,7 @@ import { dieNormal, random, randomBetweenInclusive, randomChoice, shuffle } from
 import { registerAI } from '$lib/simulate';
 import { Entrance, doorTemplate, entranceQuery } from './door';
 import { humanTemplate } from './human';
-import { drawRectangle, fillRectangle, floorTemplate, spawnNearby, spawnVault, vault } from './mapgen';
+import { drawRectangle, fillRectangle, floorTemplate, spawnNearby, spawnVault, spawnVaultRandom, vault } from './mapgen';
 import { Mission, registerMissionTemplate } from './mission';
 import { TalkNPC, registerTalkNPC } from './npc';
 import { stairDownTemplate, stairUpTemplate } from './stairs';
@@ -65,7 +65,6 @@ registerMissionTemplate('intro', {
 
 const lobbyFloor = {
   generate: (world, z, width, height, stairwells) => {
-    console.log('lobby');
     if (stairwells !== undefined) {
       console.error("Lobby isn't supposed to have predefined stairwell positions!");
       return;
@@ -74,12 +73,12 @@ const lobbyFloor = {
     drawRectangle(world, wallTemplate, 0, 0, width-1, height-1, z);
     const [x, y] = placeEntrance(world, width, height, z);
     spawnNearby(world, introRecruiterTemplate, x, y, z);
-    spawnVault(world, stairwell0VaultTemplate, 1, 1, width-2, height-2, z);
+    const result = spawnVaultRandom(world, stairwell0VaultTemplate, 1, 1, width-2, height-2, z);
     // const stairwell = stairwells[0];
     // placeMainEntrance()
     // placeBackEntrance()
     // connectRooms()
-    return ;
+    return result;
   },
 };
 
@@ -113,14 +112,19 @@ const stairwell0VaultTemplate = vault(`
 #< ##
 #   +
 #####
-`, mapping);
+`, mapping, {
+  toReturn: '<',
+});
 
 const stairwellVaultTemplate = vault(`
 ####
 #<>##
 #   +
 #####
-`, mapping);
+`, mapping, {
+  toReturn: '<',
+  anchor: [1, 1],
+});
 
 const introRecruiterTemplate = mergeTemplates(humanTemplate, [
   [AI],
@@ -150,11 +154,20 @@ registerTalkNPC('introRecruiter', [
 const bossFloor = {
   minSize: [30, 30],
   maxSize: [50, 80],
-  generate: (world, z, width, height, stairwells) => {
+  generate: (world, z, width, height, stairwell) => {
   },
 };
 
 const officeFloor = {
-  generate: (world, z, width, height, stairwells) => {
+  generate: (world, z, width, height, stairwell) => {
+    if (stairwell === undefined) {
+      console.error("Non-lobby floor should have predefined stairwell positions!");
+      return;
+    }
+    fillRectangle(world, floorTemplate, 0, 0, width-1, height-1, z);
+    drawRectangle(world, wallTemplate, 0, 0, width-1, height-1, z);
+    const {x, y} = getPos3(stairwell);
+    const result = spawnVault(world, stairwellVaultTemplate, x, y, z);
+    return result;
   },
 };

+ 48 - 12
src/routes/djii/mapgen.ts

@@ -60,30 +60,65 @@ export const spawnNearby = (world, tmplt, x, y, z) => {
     throw new Error("Couldn't spawn nearby!");
 };
 
-export const vault = (tmplt: string, mapping) => {
+export type VaultOptions = {
+  toReturn?: string,
+  anchor?: [number, number],
+};
+
+export const vault = (tmplt: string, mapping, options: VaultOptions) => {
   const glyphs = tmplt.split('\n').filter((c) => c !== '');
   const width = glyphs[0].length;
   const height = glyphs.length;
-  const spawn = (world, x0, y0, z) => glyphs.forEach((line, dy) => {
-    const y = y0+dy;
-    line.split('').forEach((glyph, dx) => {
-      const x = x0+dx;
-      if (mapping[glyph]) {
-        const e = fromTemplate(world, mapping[glyph]);
-        placeOnMap(world, e, x, y, z);
-      }
+  const spawn = (world, x0, y0, z) => {
+    let result;
+    glyphs.forEach((line, dy) => {
+      const y = y0+dy;
+      line.split('').forEach((glyph, dx) => {
+        const x = x0+dx;
+        if (mapping[glyph]) {
+          const e = fromTemplate(world, mapping[glyph]);
+          placeOnMap(world, e, x, y, z);
+          const r = options.toReturn?.indexOf(glyph);
+          if (r !== undefined && r >= 0) {
+            result = e;
+          }
+        }
+      });
     });
-  });
+    return result;
+  }
   return {
     width,
     height,
     spawn,
+    anchor: options.anchor ?? [0, 0],
   };
 };
 
-export const spawnVault = (world, tmplt, x0, y0, width, height, z) => {
+export const spawnVault = (world, tmplt, x0, y0, z) => {
+  const [dx0, dy0] = tmplt.anchor;
+  const x = x0 - dx0;
+  const y = y0 - dy0;
+  let placeOk = true;
+  for (let dy = 0; dy < tmplt.height; ++dy) {
+    for (let dx = 0; dx < tmplt.width; ++dx) {
+      if (!canPlace(world, x + dx, y + dy, z)) {
+        console.warn(`can't place on ${x} ${y}`);
+        placeOk = false;
+        break;
+      }
+    }
+  }
+  if (placeOk) {
+    return tmplt.spawn(world, x, y, z);
+  }
+  throw new Error(`Couldn't place vault at {x0} {y0} {z}`);
+};
+
+export const spawnVaultRandom = (world, tmplt, x0, y0, width, height, z) => {
   let placed = false;
   let retry = 0;
+  let result;
   while (!placed && retry < 256) {
     const x = x0 + Math.floor(random()*(width-tmplt.width));
     const y = y0 + Math.floor(random()*(height-tmplt.height));
@@ -98,13 +133,14 @@ export const spawnVault = (world, tmplt, x0, y0, width, height, z) => {
       }
     }
     if (placeOk) {
-      tmplt.spawn(world, x, y, z);
+      result = tmplt.spawn(world, x, y, z);
       placed = true;
     }
     ++retry;
   }
   if (!placed)
     throw new Error("Couldn't place a vault!");
+  return result;
 };
 
 const canPlace = (world: IWorld, x, y, z) => {

+ 12 - 8
src/routes/djii/profession.ts

@@ -1,15 +1,16 @@
 import { cottonScarf, fedora, hood, jeans, leatherGloves, leatherJacket, silentSneakers } from './clothes';
 import { lockpick, pocketKnife } from './tools';
 
-const professions = new Map();
+const professions = [];
 
-export const enumProfessions = () => Array.from(professions.entries());
+export const enumProfessions = () => professions;
 
-export const defineProfession = (name, startKit) => {
-  professions.set(name, startKit);
+export const defineProfession = (prof) => {
+  professions.push(prof);
 };
 
-defineProfession('private investigator', {
+defineProfession({
+  name: 'private investigator',
   description: "You discovered more than one secret and clinged to your old-school profession as long as you could",
   items: [
     [lockpick, 5],
@@ -25,7 +26,8 @@ defineProfession('private investigator', {
   },
 });
 
-defineProfession('thief', {
+defineProfession({
+  name: 'thief',
   description: "You kept up good ol' stealing through the roughest times, but it's an uphill battle",
   items: [
     [lockpick, 5],
@@ -48,7 +50,8 @@ defineProfession('thief', {
 
 /*
 
-defineProfession('drug courier', {
+defineProfession({
+  name: 'drug courier',
   description: "You used to deliver drugs discreetly and effectively, but couldn't survive competition from drones",
   items: [
     [joint],
@@ -65,7 +68,8 @@ defineProfession('drug courier', {
   },
 });
 
-defineProfession('barista', {
+defineProfession({
+  name: 'barista',
   description: "Coffee is your passion that you enjoyed sharing with strangers.",
   items: [
     [eliteCoffeeBeans, 5],

+ 1 - 1
src/routes/djii/style.css

@@ -40,7 +40,7 @@ button {
   border: none;
   padding: 0;
   font: inherit;
-  outline: inherit;
+  /* outline: inherit; */
 }
 
 button:hover {

+ 1 - 14
src/routes/mole/Game.svelte

@@ -71,23 +71,10 @@
   });
 
   onMount(() => {
-    // prepareMap(world);
     update();
     tileMapWidget.focus();
   });
 
-  $effect(() => {
-    // console.log(Object.entries(Glyph));
-    // console.log(entityToObject(world, getPlayer(world)));
-    // console.log(JSON.stringify(worldToObject(world)).length);
-    // const e = addEntity(world);
-    // addComponent(world, Inventory, e);
-    // console.log(JSON.stringify(entityToObject(world, e)));
-    // const serializer = defineSerializer([Interesting]);
-    // const deserializer = defineDeserializer(world);
-    // console.log(serializer(world));
-  });
-
   const onKeyPress = (ev) => {
     if (ev.key === ']') {
       const focused = logWidget.focus();
@@ -95,7 +82,7 @@
         tileMapWidget.focus();
     } else if (ev.key === '[') {
       console.log('[');
-      const focused = leftSideInfo.focus();nn
+      const focused = leftSideInfo.focus();
       if (!focused)
         tileMapWidget.focus();
     }