wip
This commit is contained in:
@@ -71,10 +71,11 @@ pub(crate) enum Action {
|
||||
/// Shove the pushable chain starting at `(x, y)` one step in `dir` (acts on
|
||||
/// arbitrary cells, not the source object). Zero time cost.
|
||||
Push { x: i32, y: i32, dir: Direction },
|
||||
/// Apply a batch of one-way solid moves simultaneously (read-all, then
|
||||
/// write-all, so cycles/swaps work). Each tuple is `(src_x, src_y, dst_x,
|
||||
/// dst_y)`. Zero time cost.
|
||||
Swap(Vec<(i32, i32, i32, i32)>),
|
||||
/// Shift a set of cells, given as `(x, y)` coordinates: each cell moves to the
|
||||
/// next coordinate in the list, unless it can't move, or that cell is blocked.
|
||||
/// Rotates the contents of the last cell back to the beginning.
|
||||
/// Zero time cost.
|
||||
Shift(Vec<(i32, i32)>),
|
||||
/// Add `n` to the player's gem count (negative subtracts; the count is
|
||||
/// clamped at 0). Zero time cost. Applied to `GameState::player_gems`.
|
||||
AddGems(i64),
|
||||
@@ -182,9 +183,9 @@ pub(crate) fn action_to_map(action: &Action) -> rhai::Map {
|
||||
m.insert("y".into(), Dynamic::from(*y as i64));
|
||||
m.insert("dir".into(), ds(dir_to_str(*dir)));
|
||||
}
|
||||
// Swap pairs are not inspectable via the queue map API; emit type only.
|
||||
Action::Swap(_) => {
|
||||
m.insert("type".into(), ds("Swap"));
|
||||
// Shift cells are not inspectable via the queue map API; emit type only.
|
||||
Action::Shift(_) => {
|
||||
m.insert("type".into(), ds("Shift"));
|
||||
}
|
||||
Action::AddGems(n) => {
|
||||
m.insert("type".into(), ds("AddGems"));
|
||||
|
||||
@@ -157,7 +157,7 @@ impl Archetype {
|
||||
pub fn default_glyph(&self) -> Glyph {
|
||||
match self {
|
||||
Archetype::Empty => Glyph {
|
||||
tile: 32,
|
||||
tile: 0,
|
||||
fg: Rgba8 { r: 0, g: 0, b: 0, a: 255 },
|
||||
bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 },
|
||||
},
|
||||
|
||||
+64
-146
@@ -96,6 +96,15 @@ impl Board {
|
||||
&mut self.layers[z].cells[y * w + x]
|
||||
}
|
||||
|
||||
/// Replace the solid (if any) at `(x, y)` with `Empty`
|
||||
pub fn clear_solid(&mut self, x: usize, y: usize) {
|
||||
if self.in_bounds((x as i32, y as i32)) {
|
||||
if let Some(z) = self.solid_cell_layer(x, y) {
|
||||
*self.get_mut(z, x, y) = (Archetype::Empty.default_glyph(), Archetype::Empty)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the glyph to display at `(x, y)`, honoring layer draw order.
|
||||
///
|
||||
/// The player is always drawn on top (it is not part of the layer stack yet).
|
||||
@@ -524,165 +533,74 @@ impl Board {
|
||||
self.add_object(obj);
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies a batch of one-way solid moves **simultaneously**, returning any
|
||||
/// nonfatal error lines (out-of-bounds entries / a write blocked by the player).
|
||||
///
|
||||
/// Each tuple is `(src_x, src_y, dst_x, dst_y)`: the solid occupant of `(src_x,
|
||||
/// src_y)` — the player, a solid object, or a terrain crate/wall — moves to
|
||||
/// `(dst_x, dst_y)`. A source with no solid moves an "empty", which **removes**
|
||||
/// whatever solid was at the destination. Every source is read before any
|
||||
/// destination is written, so cyclic permutations and two-cell swaps resolve
|
||||
/// correctly (e.g. `[a→b],[b→a]` swaps `a` and `b`).
|
||||
///
|
||||
/// Displacement rules: a destination's prior solid that isn't itself being moved
|
||||
/// is removed (terrain cleared; a scripted object despawned via
|
||||
/// [`remove_object`](Board::remove_object)). The **player is never destroyed** —
|
||||
/// a write that would overwrite the player without relocating it is skipped and
|
||||
/// logged (the player wins its cell, per the one-solid-per-cell invariant).
|
||||
///
|
||||
/// A solid refused onto the player is left at its source cell, which another
|
||||
/// entry may also target; a final sweep over the swapped cells resolves any
|
||||
/// such **overlap** — it keeps one solid, deletes the rest (never the player),
|
||||
/// and logs an error per deletion.
|
||||
pub fn apply_swap(&mut self, pairs: &[(i32, i32, i32, i32)]) -> Vec<LogLine> {
|
||||
let mut errors = Vec::new();
|
||||
|
||||
// 1. Validate: keep only entries whose source and destination are in bounds.
|
||||
let mut valid: Vec<((i32, i32), (i32, i32))> = Vec::new();
|
||||
for &(sx, sy, dx, dy) in pairs {
|
||||
if !self.in_bounds((sx, sy)) || !self.in_bounds((dx, dy)) {
|
||||
errors.push(LogLine::error(format!(
|
||||
"swap: out-of-bounds entry ({sx},{sy})->({dx},{dy})"
|
||||
)));
|
||||
continue;
|
||||
}
|
||||
valid.push(((sx, sy), (dx, dy)));
|
||||
|
||||
/// Shifts a set of cells, given as `(x, y)` coordinates. Backs the script
|
||||
/// `shift()` fn. Returns any errors as [`LogLine`]s for the caller to log.
|
||||
pub fn apply_shift(&mut self, cells: &[(i32, i32)]) -> Vec<LogLine> {
|
||||
// Validate all the cells are in bounds, error if not:
|
||||
if cells.iter().any(|&c| !self.in_bounds(c)) {
|
||||
return vec![LogLine::error("Called shift() with a cell out of bounds")]
|
||||
}
|
||||
|
||||
// 2. Snapshot the solid at each unique source (read phase). Reading every
|
||||
// source before any write is what lets cycles/swaps resolve. `None` is an
|
||||
// empty source (the old `SolidSnapshot::Empty`).
|
||||
let mut snapshots: HashMap<(i32, i32), Option<Solid>> = HashMap::new();
|
||||
for &(src, _) in &valid {
|
||||
snapshots.entry(src).or_insert_with(|| {
|
||||
self.solid_at(src.0 as usize, src.1 as usize)
|
||||
});
|
||||
}
|
||||
// Get all the Solids at these cells:
|
||||
let solids: Vec<_> = cells.iter().map(|&c| self.solid_at(c.0 as usize, c.1 as usize)).collect();
|
||||
|
||||
// 3. Compute the final occupant of every affected cell. A source that is not
|
||||
// anyone's destination is vacated (None); each entry writes its source's
|
||||
// snapshot into its destination (a later entry wins a repeated destination).
|
||||
let dsts: HashSet<(i32, i32)> = valid.iter().map(|&(_, d)| d).collect();
|
||||
let mut final_state: HashMap<(i32, i32), Option<Solid>> = HashMap::new();
|
||||
for &(src, _) in &valid {
|
||||
if !dsts.contains(&src) {
|
||||
final_state.insert(src, None);
|
||||
}
|
||||
}
|
||||
for &(src, dst) in &valid {
|
||||
final_state.insert(dst, snapshots[&src]);
|
||||
}
|
||||
// Tell whether these are normally pushable in this direction. This doesn't count whether
|
||||
// their target zone will be empty, just whether they would be willing to move that way at
|
||||
// all. The direction comes in because of hcrates / vcrates: if their target cell is adjacent,
|
||||
// then we'll check directions
|
||||
//let mut pushable: Vec<bool> = Vec::with_capacity(solids.len());
|
||||
|
||||
// The player's final cell: where a Player snapshot is installed, else its
|
||||
// current cell (it stays put). Used to protect the player from being
|
||||
// overwritten — computed up front so it's stable across the write loop.
|
||||
let player_final = final_state
|
||||
.iter()
|
||||
.find(|(_, s)| s.is_some_and(|s| s.player()))
|
||||
.map(|(&c, _)| c)
|
||||
.unwrap_or((self.player.x, self.player.y));
|
||||
let mut immobile = HashSet::new();
|
||||
for (curr_idx, curr) in solids.iter().enumerate() {
|
||||
let origin = cells[curr_idx];
|
||||
let target = cells[(curr_idx + 1) % cells.len()];
|
||||
|
||||
// 4a. Clear each affected cell's current occupant, despawning any object that
|
||||
// doesn't survive (isn't reused in final_state). The player and surviving
|
||||
// objects keep their entity and are repositioned by the install step.
|
||||
let survivors: HashSet<ObjectId> = final_state
|
||||
.values()
|
||||
.filter_map(|s| s.and_then(|s| s.object_id()))
|
||||
.collect();
|
||||
let affected: HashSet<(i32, i32)> = snapshots
|
||||
.keys()
|
||||
.copied()
|
||||
.chain(final_state.keys().copied())
|
||||
.collect();
|
||||
for &(cx, cy) in &affected {
|
||||
let (ux, uy) = (cx as usize, cy as usize);
|
||||
match self.solid_at(ux, uy) {
|
||||
Some(s) if s.object_id().is_some_and(|id| !survivors.contains(&id)) => {
|
||||
self.remove_object(s.object_id().unwrap());
|
||||
}
|
||||
Some(s) if s.archetype().is_some() => {
|
||||
if let Some(z) = self.solid_cell_layer(ux, uy) {
|
||||
*self.get_mut(z, ux, uy) = (Glyph::transparent(), Archetype::Empty);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
// Whether these represent a single-cell h or v move.
|
||||
let hmove = target.1 == origin.1 && (target.0 - origin.0).abs() == 1;
|
||||
let vmove = target.0 == origin.0 && (target.1 - origin.1).abs() == 1;
|
||||
|
||||
// We are never allowed to push a thing that won't push
|
||||
// We won't push a horizontal-only thing vertically
|
||||
// We won't push a vertical-only thing horizontally
|
||||
let pushable = curr.as_ref().map_or(Pushable::Any, |c| c.pushable());
|
||||
if pushable == Pushable::No ||
|
||||
pushable == Pushable::Vertical && hmove ||
|
||||
pushable == Pushable::Horizontal && vmove {
|
||||
immobile.insert(curr_idx);
|
||||
}
|
||||
}
|
||||
|
||||
// 4b. Install each cell's computed occupant.
|
||||
for (&(cx, cy), snap) in &final_state {
|
||||
let (ux, uy) = (cx as usize, cy as usize);
|
||||
// Empty cells were already cleared in phase 4a.
|
||||
let Some(solid) = snap else { continue };
|
||||
// The player wins its cell: never overwrite player_final with anything
|
||||
// other than the player itself. A refused solid stays at its source
|
||||
// cell; the overlap sweep below cleans up if that cell is now also
|
||||
// someone else's destination.
|
||||
if (cx, cy) == player_final && !solid.player() {
|
||||
errors.push(LogLine::error(format!(
|
||||
"swap: cannot overwrite the player at ({cx},{cy})"
|
||||
)));
|
||||
continue;
|
||||
}
|
||||
solid.place(self, ux, uy);
|
||||
}
|
||||
|
||||
// 5. Overlap sweep: a solid refused onto the player (above) stays at its
|
||||
// source cell, which may now also be a destination another entry wrote
|
||||
// into. For any swapped cell holding more than one solid, keep a single
|
||||
// occupant and delete the rest (never the player). Logs an error per
|
||||
// deletion.
|
||||
for &(cx, cy) in &affected {
|
||||
let (ux, uy) = (cx as usize, cy as usize);
|
||||
let player_here = self.player.x == cx && self.player.y == cy;
|
||||
// Solid objects on this cell; keep the first, delete the rest.
|
||||
let objs: Vec<ObjectId> = self
|
||||
.objects
|
||||
.iter()
|
||||
.filter(|(_, o)| o.x == ux && o.y == uy && o.solid)
|
||||
.map(|(&id, _)| id)
|
||||
.collect();
|
||||
let has_terrain = self.solid_cell_layer(ux, uy).is_some();
|
||||
|
||||
// How many solids share the cell (player + solid objects + solid terrain).
|
||||
let total = player_here as usize + objs.len() + has_terrain as usize;
|
||||
if total <= 1 {
|
||||
continue;
|
||||
}
|
||||
errors.push(LogLine::error(format!(
|
||||
"swap: {total} solids overlap at ({cx},{cy}); deleting extras"
|
||||
)));
|
||||
|
||||
// Pick the survivor (priority: player > first object > terrain) and
|
||||
// delete every other solid. The player is only ever a survivor, so it
|
||||
// is never deleted. `keep_obj` is the object we keep, if any.
|
||||
let keep_obj = (!player_here).then(|| objs.first().copied()).flatten();
|
||||
for id in &objs {
|
||||
if Some(*id) != keep_obj {
|
||||
self.remove_object(*id);
|
||||
// Trace back from each immobile until we find an empty:
|
||||
let mut blocked = HashSet::new();
|
||||
for curr_idx in immobile {
|
||||
let mut prev_idx = curr_idx;
|
||||
loop {
|
||||
if solids[prev_idx].is_some() && !blocked.contains(&prev_idx) {
|
||||
blocked.insert(prev_idx);
|
||||
prev_idx = (prev_idx + cells.len() - 1) % cells.len();
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
// Clear terrain unless it's the sole survivor (no player, no kept object).
|
||||
if has_terrain
|
||||
&& (player_here || keep_obj.is_some())
|
||||
&& let Some(z) = self.solid_cell_layer(ux, uy)
|
||||
{
|
||||
*self.get_mut(z, ux, uy) = (Glyph::transparent(), Archetype::Empty);
|
||||
}
|
||||
|
||||
// Clear all the cells so everything can be placed:
|
||||
for (curr_idx, curr) in cells.iter().enumerate() {
|
||||
if !blocked.contains(&curr_idx) {
|
||||
self.clear_solid(curr.0 as usize, curr.1 as usize);
|
||||
}
|
||||
}
|
||||
|
||||
errors
|
||||
// Now, move anything that we've decided is not blocked:
|
||||
for (curr_idx, curr) in solids.iter().enumerate() {
|
||||
if let Some(solid) = curr && !blocked.contains(&curr_idx) {
|
||||
let target = cells[(curr_idx + 1) % cells.len()];
|
||||
solid.place(self, target.0 as usize, target.1 as usize);
|
||||
}
|
||||
}
|
||||
|
||||
vec![]
|
||||
}
|
||||
|
||||
/// Returns the index of the layer whose terrain cell at `(x, y)` is non-`Empty`
|
||||
|
||||
@@ -308,10 +308,9 @@ impl GameState {
|
||||
board.push(x as usize, y as usize, dir);
|
||||
}
|
||||
}
|
||||
// apply_swap reads all sources then writes all destinations, so a
|
||||
// whole batch of moves resolves simultaneously (cycles included).
|
||||
Action::Swap(pairs) => {
|
||||
logs.extend(board.apply_swap(&pairs));
|
||||
// apply_shift moves the named cells.
|
||||
Action::Shift(cells) => {
|
||||
logs.extend(board.apply_shift(&cells));
|
||||
}
|
||||
// Accumulated and applied to `self.player_gems` after the borrow drops.
|
||||
Action::AddGems(n) => gem_delta += n,
|
||||
|
||||
+23
-25
@@ -1107,37 +1107,35 @@ fn register_write_api(engine: &mut Engine, queues: &QueueMap) {
|
||||
},
|
||||
);
|
||||
|
||||
// swap(pairs): apply a batch of one-way solid moves simultaneously. `pairs` is
|
||||
// an array whose elements are four-int arrays `[src_x, src_y, dst_x, dst_y]`. A
|
||||
// malformed entry is skipped with a logged error. Zero time cost.
|
||||
// shift([[x, y], ...): Emits a shift action which moves the contents of each given cell in
|
||||
// a loop (the last cell is moved to the first coord). Doesn't move things that aren't pushable,
|
||||
// and won't move anything into a cell that's not vacant (or vacated by this shift).
|
||||
let q = queues.clone();
|
||||
engine.register_fn("swap", move |ctx: NativeCallContext, arr: Array| {
|
||||
engine.register_fn("shift", move |ctx: NativeCallContext, arr: Array| {
|
||||
let src = source_of(&ctx);
|
||||
let mut pairs: Vec<(i32, i32, i32, i32)> = Vec::new();
|
||||
for elem in &arr {
|
||||
// Each entry must be an array of exactly four integers.
|
||||
let coords: Option<Vec<i64>> = elem.read_lock::<Array>().and_then(|inner| {
|
||||
if inner.len() == 4 {
|
||||
inner.iter().map(|v| v.as_int().ok()).collect()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
match coords {
|
||||
Some(c) => pairs.push((c[0] as i32, c[1] as i32, c[2] as i32, c[3] as i32)),
|
||||
None => emit(
|
||||
&q,
|
||||
src,
|
||||
Action::Log(LogLine::error(
|
||||
"swap: each entry must be [src_x, src_y, dst_x, dst_y]".to_string(),
|
||||
)),
|
||||
),
|
||||
}
|
||||
match read_coord_array(&arr) {
|
||||
Ok(pairs) => emit(&q, src, Action::Shift(pairs)),
|
||||
Err(_) => emit(&q, src, Action::Log(LogLine::error("shift: each entry must be [x, y]".to_string())))
|
||||
}
|
||||
emit(&q, src, Action::Swap(pairs));
|
||||
});
|
||||
}
|
||||
|
||||
/// Read a `Vec<(i32, i32)>` from a Rhai array, to receive a list of coordinates from a script.
|
||||
/// Returns Err if the array isn't `[[x, y], ...]`
|
||||
fn read_coord_array(arr: &Array) -> Result<Vec<(i32, i32)>, ()> {
|
||||
let mut pairs: Vec<(i32, i32)> = Vec::new();
|
||||
for elem in arr {
|
||||
let coords: Option<Vec<i64>> = elem.read_lock::<Array>().and_then(|inner| {
|
||||
inner.iter().map(|v| v.as_int().ok()).collect()
|
||||
});
|
||||
if let Some(coords) = coords && coords.len() == 2 {
|
||||
pairs.push((coords[0] as i32, coords[1] as i32))
|
||||
} else {
|
||||
return Err(());
|
||||
}
|
||||
}
|
||||
Ok(pairs)
|
||||
}
|
||||
// ── Queue API ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn register_queue_api(engine: &mut Engine) {
|
||||
|
||||
@@ -16,30 +16,30 @@ fn tick(dt) {
|
||||
// Direction from the builtin tag; clockwise unless explicitly counter-clockwise.
|
||||
let cw = !Me.has_tag("BUILTIN_spinner_ccw");
|
||||
|
||||
let ox = Me.x;
|
||||
let oy = Me.y;
|
||||
|
||||
// The 8 neighbour offsets in clockwise order, starting at North.
|
||||
let ring = [
|
||||
[ 0, -1], // N
|
||||
[ 1, -1], // NE
|
||||
[ 1, 0], // E
|
||||
[ 1, 1], // SE
|
||||
[ 0, 1], // S
|
||||
[-1, 1], // SW
|
||||
[-1, 0], // W
|
||||
[-1, -1], // NW
|
||||
let cw_ring = [
|
||||
[Me.x, Me.y-1], // N
|
||||
[Me.x+1, Me.y-1], // NE
|
||||
[Me.x+1, Me.y], // E
|
||||
[Me.x+1, Me.y+1], // SE
|
||||
[Me.x, Me.y+1], // S
|
||||
[Me.x-1, Me.y+1], // SW
|
||||
[Me.x-1, Me.y], // W
|
||||
[Me.x-1, Me.y-1], // NW
|
||||
];
|
||||
|
||||
// The cardinal step that carries each ring cell to the *next* slot in the spin
|
||||
// direction, and the index offset to that slot (+1 clockwise, -1 == +7 counter).
|
||||
// CW: N->NE is East, NE->E is South, ... CCW: N->NW is West, NE->N is West, ...
|
||||
let dirs = if cw {
|
||||
[East, South, South, West, West, North, North, East]
|
||||
// For CCW, it's the same list in reverse
|
||||
let ccw_ring = [];
|
||||
for neighbor in cw_ring {
|
||||
ccw_ring.insert(0, neighbor);
|
||||
}
|
||||
|
||||
// Rotate the neighbor cells:
|
||||
if cw {
|
||||
shift(cw_ring);
|
||||
} else {
|
||||
[West, West, North, North, East, East, South, South]
|
||||
};
|
||||
let step = if cw { 1 } else { 7 };
|
||||
shift(ccw_ring);
|
||||
}
|
||||
|
||||
// Animate the glyph one frame per rotation (changing only the character): the
|
||||
// line spins '/'-'\'-, slash-swapped for counter-clockwise. Frame state lives in
|
||||
@@ -50,55 +50,5 @@ fn tick(dt) {
|
||||
set_tile(frames[f % 4]);
|
||||
Registry.set(fkey, (f + 1) % 4);
|
||||
|
||||
// moves[i]: will ring cell i rotate into its destination slot? Seed with
|
||||
// can_shift, which is true only when cell i holds a pushable whose immediate
|
||||
// next cell isn't a wall (so it could move *if* that next cell ends up free).
|
||||
let moves = [];
|
||||
for i in 0..8 {
|
||||
let sx = ox + ring[i][0];
|
||||
let sy = oy + ring[i][1];
|
||||
moves.push(can_shift(sx, sy, dirs[i]));
|
||||
}
|
||||
|
||||
// Cascade: a cell may only move if its destination is a hole (`passable`), or
|
||||
// the occupant of that destination also moves. Demote until stable. A broken
|
||||
// arc collapses back from the jam; a full pushable cycle never demotes (every
|
||||
// destination's occupant moves) and rotates whole via swap's cycle handling.
|
||||
loop {
|
||||
let changed = false;
|
||||
for i in 0..8 {
|
||||
let n = (i + step) % 8;
|
||||
let pn = (i + step - 1) % 8;
|
||||
let dx = ox + ring[n][0];
|
||||
let dy = oy + ring[n][1];
|
||||
let sx = ox + ring[pn][0];
|
||||
let sy = oy + ring[pn][1];
|
||||
|
||||
let blocked = !passable(dx, dy) && !combinable(sx, sy, dx, dy);
|
||||
|
||||
if moves[i] && blocked && !moves[n] {
|
||||
moves[i] = false;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if !changed { break; }
|
||||
}
|
||||
|
||||
// Emit the surviving rotations as one simultaneous batch, then pace to twice
|
||||
// a second. Coordinates are pulled into locals first to keep each expression
|
||||
// simple (Rhai caps expression complexity).
|
||||
let batch = [];
|
||||
for i in 0..8 {
|
||||
if moves[i] {
|
||||
let n = (i + step) % 8;
|
||||
let sx = ox + ring[i][0];
|
||||
let sy = oy + ring[i][1];
|
||||
let dx = ox + ring[n][0];
|
||||
let dy = oy + ring[n][1];
|
||||
batch.push([sx, sy, dx, dy]);
|
||||
}
|
||||
}
|
||||
|
||||
swap(batch);
|
||||
delay(0.5);
|
||||
}
|
||||
|
||||
+15
-2
@@ -167,6 +167,18 @@ fn tick(dt) {
|
||||
}
|
||||
"""
|
||||
|
||||
shifter = """
|
||||
fn tick(dt) {
|
||||
let x = Me.x;
|
||||
let y = Me.y;
|
||||
|
||||
if Queue.length() == 0 {
|
||||
shift([[x-1, y], [x, y-1], [x+1, y], [x, y+1]]);
|
||||
delay(0.25);
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
[boards.start.map]
|
||||
name = "Starting Room"
|
||||
width = 60
|
||||
@@ -214,8 +226,8 @@ gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
|
||||
content = """
|
||||
############################################################
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# o #
|
||||
# oS #
|
||||
# #
|
||||
# 1 #
|
||||
# ###### B #
|
||||
@@ -252,6 +264,7 @@ content = """
|
||||
"V" = { kind = "object", tile = 1, fg = "#33aa33", bg = "#000000", script_name = "mover" }
|
||||
"M" = { kind = "object", tile = 15, fg = "#ffdd88", bg = "#000000", solid = true, name = "muffin", script_name = "muffin" }
|
||||
"B" = { kind = "object", tile = 240, fg = "#cc9944", bg = "#000000", solid = true, name = "noticeboard", script_name = "noticeboard" }
|
||||
"S" = { kind = "object", tile = 240, fg = "#cc9944", bg = "#000000", solid = true, name = "shifter", script_name = "shifter" }
|
||||
"/" = { kind = "spinner_cw" }
|
||||
"t" = { kind = "gem" }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user