16 次代碼提交 5ccaffe4da ... 326e00ddfe

作者 SHA1 備註 提交日期
  caryoscelus 326e00ddfe DJII: fix profession item spawning, show profession in status, item descriptions 10 月之前
  caryoscelus 1b59f21aa7 Fix Select index 10 月之前
  caryoscelus 9256a6b184 DJII: basic inventory 10 月之前
  caryoscelus fcb11828b1 DJII: floor description 10 月之前
  caryoscelus 3c6f4e2067 Select component: allow access by index 10 月之前
  caryoscelus 79e3f9fc59 Inventory improvements 10 月之前
  caryoscelus 4e9b43dc1e DJII: attacking WIP 10 月之前
  caryoscelus 265074cba7 DJII: bit of intro 10 月之前
  caryoscelus f5a5b23f3d DJII: basic map zoom 10 月之前
  caryoscelus 7f85748657 DJII: thugs WIP 10 月之前
  caryoscelus 2514caf450 Use TS-style notNullish 10 月之前
  caryoscelus 001c81fb5e DJII: Skills 10 月之前
  caryoscelus 842d22a4ee Generalize ItemList 10 月之前
  caryoscelus b07a68561e DJII CharStatus WIP 10 月之前
  caryoscelus 40c23111b4 Support Pos3 in entity direction indicator 10 月之前
  caryoscelus fd4b8b178f Hide non-graphic entities from entity list 10 月之前

+ 13 - 4
src/lib/inventory.ts

@@ -18,12 +18,11 @@ export const Stored = defineComponent('Stored', {
   parent: Types.eid,
 });
 
-export const getInventory = (world) => {
-  const pc = playerQuery(world)[0];
-  const sz = Inventory.count[pc];
+export const getInventory = (world, char) => {
+  const sz = Inventory.count[char];
   let res = [];
   for (let i = 0; i < sz; ++i) {
-    res.push(Inventory.inv[pc][i]);
+    res.push(Inventory.inv[char][i]);
   };
   return res;
 };
@@ -74,6 +73,16 @@ export const DropAction = (item) => (world, e) => {
 
 registerAction([Stored], 'drop', DropAction);
 
+export const addToInventory = (world, e, item) => {
+  const ix = Inventory.count[e];
+  if (ix >= Inventory.size[e]) {
+    return false;
+  }
+  Inventory.inv[e][ix] = item;
+  ++Inventory.count[e];
+  return true;
+};
+
 export const removeFromInventory = (world, e, item) => {
   const ix = Inventory.inv[e].indexOf(item);
   if (ix < 0) {

+ 0 - 2
src/lib/simulate.ts

@@ -60,7 +60,6 @@ export const stepActors = (world, cmd) => {
         stepActor(world, e, cmd);
         cmd = undefined;
       } else if (hasComponent(world, AI, e)) {
-        console.log('AI');
         const action = findAIAction(world, e);
         if (action) {
           stepActor(world, e, action);
@@ -69,7 +68,6 @@ export const stepActors = (world, cmd) => {
           Clock.time[e] += 1;
         }
       } else {
-        console.log('Neither player nor AI');
         for (let component of getEntityComponents(world, e)) {
           const action = getOnTickActive(component);
           if (action) {

+ 4 - 0
src/lib/util.ts

@@ -16,3 +16,7 @@ export const makeEnum = (...names) => {
   });
   return Object.freeze(obj);
 };
+
+export function notNullish<T>(v: T | null | undefined): v is NonNullable<T> {
+    return v !== null && v !== undefined;
+}

+ 18 - 0
src/routes/Counter.svelte

@@ -0,0 +1,18 @@
+<script>
+  let { label, amount, plus, minus } = $props();
+
+  const onkeydown = (ev) => {
+    // TODO: support common custom arrow bindings
+    if (ev.key === '+' || ev.key === 'ArrowRight') {
+      plus();
+    } else if (ev.key === '-' || ev.key === 'ArrowLeft') {
+      minus();
+    }
+  };
+</script>
+
+<div tabindex="0" {onkeydown}>
+  {label} {amount}
+  <button tabindex="-1" onclick={minus}>-</button>
+  <button tabindex="-1" onclick={plus}>+</button>
+</div>

+ 20 - 5
src/routes/EntityLine.svelte

@@ -1,19 +1,30 @@
 <script>
   import { describe, getPlayer } from '$lib';
   import { Glyph } from '$lib/components';
+  import { hasComponent } from '$lib/ecs';
   import { getPos2, hasPos2 } from '$lib/pos2';
+  import { getPos3, hasPos3 } from '$lib/pos3';
   import { decode } from '$lib/util';
 
   let { item, world, onclick, selected, zoomlevel } = $props();
   let selectedClass = $derived(item === selected ? 'selected' : '');
+  let emptyClass = $derived((!debug && !hasComponent(world, Glyph, item)) ? 'empty' : '');
   let pc = $derived(getPlayer(world));
   let style = $derived(zoomlevel ? `font-size: ${zoomlevel*0.6}px` : '');
+  const debug = false;
 
   const describeDirection = (e) => {
-    if (!hasPos2(world, e))
-      return '';
-    const {x, y} = getPos2(e);
-    const pov = getPos2(pc);
+    let xy, pov;
+    if (hasPos2(world, e)) {
+      xy = getPos2(e);
+      pov = getPos2(pc);
+    } else if (hasPos3(world, e)) {
+      xy = getPos3(e);
+      pov = getPos3(pc);
+    } else {
+      return;
+    }
+    const {x, y} = xy;
     const dx = x - pov.x;
     const dy = y - pov.y;
     if (dx === 0 && dy === 0) {
@@ -33,7 +44,7 @@
   };
 </script>
 
-<div class="envitem {selectedClass}" onclick={() => onclick(item)}>
+<div class="envitem {selectedClass} {emptyClass}" onclick={() => onclick(item)}>
   <pre class="glyph" style="{style}; color: {decode(Glyph.color[item])};">{decode(Glyph.glyph[item])} </pre>
   <span {style}>
     {describe(world, item)}
@@ -52,4 +63,8 @@
   .overlay {
     margin-left: auto;
   }
+
+  .empty {
+    display: none;
+  }
 </style>

+ 42 - 0
src/routes/ItemList.svelte

@@ -0,0 +1,42 @@
+<script>
+  let widget;
+
+  const onkeydown = (ev) => {
+    // TODO: maybe there's a better way to change focus?
+    if (ev.key === 'ArrowUp') {
+      for (let child of widget.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 widget.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} bind:this={widget}>
+  <slot/>
+</div>

+ 7 - 4
src/routes/Select.svelte

@@ -1,5 +1,5 @@
 <script>
-  let { options, value } = $props();
+  let { options, index, value } = $props();
 
   const onkeydown = (ev) => {
     // TODO: support custom controls
@@ -7,9 +7,11 @@
     if (ev.key === 'ArrowUp') {
       const newIx = Math.max(0, ix-1);
       value = options[newIx];
+      index = newIx;
     } else if (ev.key === 'ArrowDown') {
       const newIx = Math.min(options.length-1, ix+1);
       value = options[newIx];
+      index = newIx;
     }
   };
 
@@ -20,14 +22,15 @@
     return '';
   };
 
-  const select = (option) => {
+  const select = (option, ix) => {
     value = option;
+    index = ix;
   };
 </script>
 
 <ul tabindex="0" {onkeydown}>
-  {#each options as option}
-    <li class="selectable {selectedClass(option)}" onclick={() => select(option)}>
+  {#each options as option, ix}
+    <li class="selectable {selectedClass(option)}" onclick={() => select(option, ix)}>
       {option.name}
     </li>
   {/each}

+ 15 - 0
src/routes/djii/BodyPartIcon.svelte

@@ -0,0 +1,15 @@
+<script>
+  let { health, selected, select, part } = $props();
+  let selectedClass = $derived(selected === part ? 'selected' : '');
+  let extraStyle = $derived(`color: color-mix(in lch, #090 ${health[part]*100}%, #900)`);
+</script>
+
+<span class="bodypart {selectedClass}" style={extraStyle} onclick={select(part)}>
+  <slot />
+</span>
+
+<style>
+  .bodypart {
+    /* font-weight: bold; */
+  }
+</style>

+ 9 - 9
src/routes/djii/CharCreation.svelte

@@ -4,9 +4,10 @@
   import { onMount } from 'svelte';
   import Select from '../Select.svelte';
   import { newGame } from './game';
-  import { enumProfessions } from './profession';
+  import { applyProfession, enumProfessions } from './profession';
   import { enumSkills } from './skill';
   import Stats from './Stats.svelte';
+  import Skills from './Skills.svelte';
 
   let { game } = $props();
 
@@ -22,6 +23,7 @@
   const professions = enumProfessions();
   const skills = enumSkills();
 
+  let professionIx = $state(0);
   let selectedProfession = $state(professions[0]);
 
   // skip char-creation (for debug purposes)
@@ -43,12 +45,15 @@
     addComponent(game0.world, Name, game0.pc, {
       name: playerName,
     });
+    applyProfession(game0.world, game0.pc, professions[professionIx]);
+    // NOTE: line below doesn't work because of Svelte reactivity conflict with bitECS
+    /* applyProfession(game0.world, game0.pc, selectedProfession); */
     game = game0;
   };
 
   const finishCharacter = () => {
     /*
-    if (freePoints > 0) {
+    if (freeStatPoints > 0) {
       error = 'Free stat points left!';
       return;
     }
@@ -62,18 +67,13 @@
   <input bind:this={charNameWidget} bind:value={playerName} /> the {selectedProfession.name}
   <div>
     <h3>Profession</h3>
-    <Select options={professions} bind:value={selectedProfession} />
+    <Select options={professions} bind:value={selectedProfession} bind:index={professionIx} />
   </div>
   <div>
     <h3>Hobby</h3>
   </div>
   <Stats game={game0} bind:tick />
-  <div>
-    <h3>Skills</h3>
-    {#each skills as [name, skill]}
-      {name}
-    {/each}
-  </div>
+  <Skills game={game0} bind:tick />
   <div>{error}</div>
   <button onclick={finishCharacter}>Ok</button>
 </section>

+ 0 - 0
src/routes/djii/CharStatus.svelte


Some files were not shown because too many files changed in this diff