itch.io is community of indie game creators and players

Devlogs

Semantic Vector Search

Totally Accurate Succubus Simulator
A browser game made in HTML5

Motivation

Suppose you’re making a visual novel with lots of images. In most VNs, each scene—say, groping in a hotel room—has a single image you assign to show. So you name your files something like:

hotel_groping.webp

And build scenes like this:

location: 'hotel'
activity: 'groping'
img: 'hotel_groping.webp'

You do this for every event—hotel_handjob, hotel_oral, home_tentacles, dream-world_sleep, and so on. You build an arsenal of lewd images in your assets folder.

But here’s the subtle problem:
If you want to test scenes before the final art is done, you need placeholders for everything. You create hundreds of placeholder files, which get replaced later.

Later, you expand: now each scene can show three images. You update all your scripts and rename all your files:

location: 'hotel'
activity: 'groping'
img: ['hotel_groping_01.webp', 'hotel_groping_02.webp', 'hotel_groping_03.webp']

Then you realize some images don’t fit—wrong hair color, odd angles, whatever—so you delete a few. But now you have to manually update every script that used them, or the game breaks.

Multiply by a thousand images, and you’re spending more time wiring assets than creating content.

AI art generation can’t save you if you’re still hand-wiring every scene.


The Solution: Use Vectors

Instead, you can name your generated image files like this:

hotel_groping_0.1301331480579081.webp
  • First part: location
  • Second part: activity
  • Third part: intensity (a number from 0 [calm] to 1 [wild])

A parser script turns each file into an entry like this:

name: 'hotel_groping_0.1301331480579081.webp'
vec: [1, 0, 0.026]
  • 1 = hotel room (location)
  • 0 = groping (activity)
  • 0.026 = intensity (normalized so every image gets a fair chance to be used)

Now imagine every image is a point in a 3D space:

  • Same location? Same row.
  • Same activity? Same column.
  • Same intensity? Same height.

After parsing, your asset table looks like:

{"name": "hotel_groping_0.1301...", "vec": [1, 0, 0.026]},
{"name": "hotel_groping_0.3127...", "vec": [1, 0, 0.289]},
{"name": "hotel_handjob_0.3659...", "vec": [1, 1, 0.214]},
// ...etc

Now your assets are mapped in a vector space—and computers are very good at finding what’s “closest” in a sea of numbers.


Querying: Let the Computer Choose

When the game needs an image, it doesn’t look for a filename—it describes the kind of image it wants:

location: 'hotel'
activity: 'handjob'
intensity: 0.4

The engine turns this into a vector:

[1, 1, 0.4]

It then finds the nearest neighbor in the asset table—like asking for the closest subway station to your destination. For example, the closest match might be [1, 1, 0.214], returning:

hotel_handjob_0.36592505330525615.webp

That’s the image you see in the game.


Why This Solves Everything (Almost)

One word: scalability.

  • Your game logic doesn’t care how many images you have; it always gets the best available match.
  • Adding images is trivial. Just drop them in the folder—they’re immediately usable.
  • No more placeholder images: the system finds the best candidate, or falls back automatically if a perfect match doesn’t exist.
  • No more manual updates after deleting or adding files. No more “missing asset” bugs.


For the Curious: Actual Code

import { abilities, locations } from './data.js';
import { table } from './table.js';
const loc_labels = {}, act_labels = {};
locations.forEach((e, i) => loc_labels[e.label] = i);
abilities.forEach((e, i) => act_labels[e.label] = i);
const w = [16, 1, 1]; // weights: prioritize location most
const sqdist = (v1, v2) => v1.map((v, i) => ((v - v2[i]) * w[i]) ** 2).reduce((a, b) => a + b);
const nearest = vec =>
    table.reduce((best, e) =>
        sqdist(e.vec, vec) < sqdist(best.vec, vec) ? e : best,
        table[0]
    );
const qer2vec = (loc, act, intensity) => [loc_labels[loc] ?? -1, act_labels[act] ?? -1, intensity];
export const query = (loc, act, intensity=.5) => nearest(qer2vec(loc, act, intensity)).name;


Why Vectors?

  • You always get a fallback: There’s always a closest match.
  • No perfect match needed: The system is flexible, robust, and automatic.
  • Works at any scale: 10 images or 10,000—no new code needed.

In short:

Vectors turn asset management from a headache into a breeze. The game always shows the most appropriate image for any scene, without ever needing you to play librarian.

Leave a comment