plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


f689ac7 — Peter John 2026-11-02T13:51:37+05:30
T6: KaM map viewer (P1)
Files changed (1) hide show
  1. plum-game/kam/Main.plum +258 -0
plum-game/kam/Main.plum ADDED
@@ -0,0 +1,258 @@
1
+ # A Knights and Merchants map viewer: terrain tiles and map objects drawn through
2
+ # the game host, a camera that pans (arrows / WASD) and zooms about the mouse
3
+ # (wheel), and a minimap in the bottom-right corner that centres the camera on click.
4
+ module main
5
+ import std/Str
6
+ import std/Int
7
+ import std/Float
8
+ import std/Bool
9
+ import std/Array
10
+ import std/Grid
11
+ import std/Vec2
12
+ import std/Color
13
+ import std/Game
14
+
15
+ primitive View =
16
+ TileSize = 32.0 # world units per tile
17
+ MinScale = 0.25
18
+ MaxScale = 4.0
19
+ PanSpeed = 600.0 # screen pixels per second
20
+ MiniSize = 160.0 # minimap's longest side in screen pixels
21
+ MiniMargin = 8.0
22
+ MapPath = "Maps/A new Beginning/A new Beginning.map"
23
+
24
+ # The inclusive tile columns and rows to draw.
25
+ struct TileRange(x0: Int, y0: Int, x1: Int, y1: Int)
26
+
27
+ # The minimap's placement on screen: top-left corner and pixels per tile.
28
+ struct MiniRect(x: Float, y: Float, cell: Float)
29
+
30
+ # World point (x, y) sits at the screen's top-left; one world unit is `scale` pixels.
31
+ record Camera(x: Float, y: Float, scale: Float) =
32
+
33
+ fun screenToWorld(self, p: Vec2) -> Vec2 =
34
+ Vec2(x: self.x + p.x / self.scale, y: self.y + p.y / self.scale)
35
+
36
+ fun worldToScreen(self, p: Vec2) -> Vec2 =
37
+ Vec2(x: {p.x - self.x} * self.scale, y: {p.y - self.y} * self.scale)
38
+
39
+ # Keeps the view inside a `w` x `h` world on a `sw` x `sh` screen; an axis the
40
+ # world doesn't fill is centred.
41
+ fun clampTo(self, w: Float, h: Float, sw: Float, sh: Float) =
42
+ vw := sw / self.scale
43
+ vh := sh / self.scale
44
+ if vw >= w
45
+ self.x = {w - vw} / 2.0
46
+ else
47
+ self.x = self.x.max(0.0).min(w - vw)
48
+ if vh >= h
49
+ self.y = {h - vh} / 2.0
50
+ else
51
+ self.y = self.y.max(0.0).min(h - vh)
52
+
53
+ # Multiplies the zoom by `factor` (scale stays within MinScale..MaxScale) keeping the
54
+ # world point under `screen` fixed.
55
+ fun zoomAt(self, screen: Vec2, factor: Float) =
56
+ anchor := self.screenToWorld(screen)
57
+ self.scale = {self.scale * factor}.max(View.MinScale).min(View.MaxScale)
58
+ self.x = anchor.x - screen.x / self.scale
59
+ self.y = anchor.y - screen.y / self.scale
60
+
61
+ # Moves the view so world point `p` is at the screen's centre.
62
+ fun centerOn(self, p: Vec2, sw: Float, sh: Float) =
63
+ self.x = p.x - sw / self.scale / 2.0
64
+ self.y = p.y - sh / self.scale / 2.0
65
+
66
+ # Tiles touching the screen, including partial ones, clamped to a `mapW` x `mapH` map.
67
+ fun visibleTiles(self, mapW: Int, mapH: Int, sw: Float, sh: Float) -> TileRange =
68
+ left := self.x.max(0.0)
69
+ top := self.y.max(0.0)
70
+ right := {self.x + sw / self.scale}.max(0.0)
71
+ bottom := {self.y + sh / self.scale}.max(0.0)
72
+ TileRange(
73
+ x0: Int(left / View.TileSize).min(mapW - 1),
74
+ y0: Int(top / View.TileSize).min(mapH - 1),
75
+ x1: Int(right / View.TileSize).min(mapW - 1),
76
+ y1: Int(bottom / View.TileSize).min(mapH - 1))
77
+
78
+ fun miniRect(sw: Float, sh: Float, mapW: Int, mapH: Int) -> MiniRect =
79
+ longest := Float(mapW.max(mapH))
80
+ cell := View.MiniSize / longest
81
+ MiniRect(
82
+ x: sw - View.MiniMargin - cell * Float(mapW),
83
+ y: sh - View.MiniMargin - cell * Float(mapH),
84
+ cell: cell)
85
+
86
+ # World point under screen point `p` of the minimap, or null when `p` is outside it.
87
+ fun miniToWorld(m: MiniRect, mapW: Int, mapH: Int, p: Vec2) -> Vec2? =
88
+ tx := {p.x - m.x} / m.cell
89
+ ty := {p.y - m.y} / m.cell
90
+ if tx < 0.0 || ty < 0.0 || tx >= Float(mapW) || ty >= Float(mapH)
91
+ return null
92
+ Vec2(x: tx * View.TileSize, y: ty * View.TileSize)
93
+
94
+ record Viewer(
95
+ cam: Camera,
96
+ ok: Bool,
97
+ mapW: Int,
98
+ mapH: Int,
99
+ tileset: Int,
100
+ trees: Int,
101
+ terrain: Grid[Int],
102
+ rotation: Grid[Int],
103
+ objects: Grid[Int],
104
+ mini: Array[Color])
105
+
106
+ export fun init() -> Viewer =
107
+ tileset := atlasLoad("Tileset")
108
+ trees := atlasLoad("Trees_a")
109
+ m := mapLoad(View.MapPath)
110
+ cam := Camera(x: 0.0, y: 0.0, scale: 1.0)
111
+ if tileset < 0 || trees < 0 || m < 0
112
+ return Viewer(
113
+ cam: cam, ok: false, mapW: 1, mapH: 1, tileset: tileset, trees: trees,
114
+ terrain: Grid.filled(1, 1, 0), rotation: Grid.filled(1, 1, 0),
115
+ objects: Grid.filled(1, 1, 0), mini: Array[Color]())
116
+ w := mapWidth(m)
117
+ h := mapHeight(m)
118
+ terrain := Grid.filled(w, h, 0)
119
+ rotation := Grid.filled(w, h, 0)
120
+ objects := Grid.filled(w, h, 0)
121
+ mini := Array[Color]()
122
+ for y := range h
123
+ for x := range w
124
+ t := mapTerrain(m, x, y)
125
+ terrain.set(x, y, t)
126
+ rotation.set(x, y, mapRotation(m, x, y))
127
+ objects.set(x, y, mapObject(m, x, y))
128
+ mini.push(spriteColor(tileset, t + 1))
129
+ Viewer(
130
+ cam: cam, ok: true, mapW: w, mapH: h, tileset: tileset, trees: trees,
131
+ terrain: terrain, rotation: rotation, objects: objects, mini: mini)
132
+
133
+ fun worldW(v: Viewer) -> Float =
134
+ Float(v.mapW) * View.TileSize
135
+
136
+ fun worldH(v: Viewer) -> Float =
137
+ Float(v.mapH) * View.TileSize
138
+
139
+ export fun update(v: Viewer, dt: Float) =
140
+ if !v.ok
141
+ return
142
+ sw := screenWidth()
143
+ sh := screenHeight()
144
+ step := View.PanSpeed * dt / v.cam.scale
145
+ if isKeyDown(Key.Left) || isKeyDown(Key.KeyA)
146
+ v.cam.x = v.cam.x - step
147
+ if isKeyDown(Key.Right) || isKeyDown(Key.KeyD)
148
+ v.cam.x = v.cam.x + step
149
+ if isKeyDown(Key.Up) || isKeyDown(Key.KeyW)
150
+ v.cam.y = v.cam.y - step
151
+ if isKeyDown(Key.Down) || isKeyDown(Key.KeyS)
152
+ v.cam.y = v.cam.y + step
153
+ wheel := mouseWheel()
154
+ if wheel != 0.0
155
+ v.cam.zoomAt(mousePos(), Float(1.1).pow(wheel))
156
+ if isMouseDown(Mouse.Left)
157
+ target := miniToWorld(miniRect(sw, sh, v.mapW, v.mapH), v.mapW, v.mapH, mousePos())
158
+ if t := target
159
+ v.cam.centerOn(t, sw, sh)
160
+ v.cam.clampTo(worldW(v), worldH(v), sw, sh)
161
+
162
+ export fun draw(v: Viewer) =
163
+ clear(Color.Black)
164
+ sw := screenWidth()
165
+ sh := screenHeight()
166
+ if !v.ok
167
+ drawText("assets not found", 20.0, 40.0, 24.0, Color.White)
168
+ return
169
+ setCamera(v.cam.x, v.cam.y, v.cam.scale)
170
+ r := v.cam.visibleTiles(v.mapW, v.mapH, sw, sh)
171
+ for y := range r.y1 - r.y0 + 1
172
+ for x := range r.x1 - r.x0 + 1
173
+ tx := r.x0 + x
174
+ ty := r.y0 + y
175
+ drawTile(v.tileset, v.terrain.get(tx, ty) + 1, Float(tx) * View.TileSize,
176
+ Float(ty) * View.TileSize, View.TileSize, v.rotation.get(tx, ty), Color.White)
177
+ for y := range r.y1 - r.y0 + 1
178
+ for x := range r.x1 - r.x0 + 1
179
+ tx := r.x0 + x
180
+ ty := r.y0 + y
181
+ o := v.objects.get(tx, ty)
182
+ if o != 0
183
+ drawSprite(v.trees, o, Float(tx) * View.TileSize, Float(ty) * View.TileSize,
184
+ false, Color.White)
185
+ resetCamera()
186
+ drawMinimap(v, sw, sh)
187
+ drawText("FPS {Int(1.0 / frameTime().max(0.0001))}", 8.0, 20.0, 20.0, Color.Yellow)
188
+
189
+ fun drawMinimap(v: Viewer, sw: Float, sh: Float) =
190
+ m := miniRect(sw, sh, v.mapW, v.mapH)
191
+ for y := range v.mapH
192
+ for x := range v.mapW
193
+ drawRect(m.x + Float(x) * m.cell, m.y + Float(y) * m.cell, m.cell, m.cell,
194
+ v.mini.get(y * v.mapW + x))
195
+ vx := m.x + v.cam.x * m.cell / View.TileSize
196
+ vy := m.y + v.cam.y * m.cell / View.TileSize
197
+ vw := sw / v.cam.scale * m.cell / View.TileSize
198
+ vh := sh / v.cam.scale * m.cell / View.TileSize
199
+ drawLine(vx, vy, vx + vw, vy, 1.0, Color.White)
200
+ drawLine(vx + vw, vy, vx + vw, vy + vh, 1.0, Color.White)
201
+ drawLine(vx + vw, vy + vh, vx, vy + vh, 1.0, Color.White)
202
+ drawLine(vx, vy + vh, vx, vy, 1.0, Color.White)
203
+
204
+ test "screenToWorld and worldToScreen are inverses"
205
+ c := Camera(x: 100.0, y: 50.0, scale: 2.0)
206
+ assert c.screenToWorld(Vec2(x: 20.0, y: 10.0)) == Vec2(x: 110.0, y: 55.0)
207
+ assert c.worldToScreen(Vec2(x: 110.0, y: 55.0)) == Vec2(x: 20.0, y: 10.0)
208
+
209
+ test "zoomAt keeps the world point under the mouse fixed and clamps the scale"
210
+ c := Camera(x: 100.0, y: 50.0, scale: 2.0)
211
+ m := Vec2(x: 300.0, y: 200.0)
212
+ before := c.screenToWorld(m)
213
+ c.zoomAt(m, 1.5)
214
+ assert c.scale == 3.0
215
+ assert c.screenToWorld(m).distance(before) < 0.000001
216
+ c.zoomAt(m, 100.0)
217
+ assert c.scale == 4.0
218
+ c.zoomAt(m, 0.0001)
219
+ assert c.scale == 0.25
220
+
221
+ test "visibleTiles covers the partial tile and clamps to the map"
222
+ c := Camera(x: 0.0, y: 0.0, scale: 1.0)
223
+ r := c.visibleTiles(64, 64, 800.0, 600.0)
224
+ assert r.x0 == 0
225
+ assert r.y0 == 0
226
+ assert r.x1 == 25
227
+ assert r.y1 == 18
228
+ small := c.visibleTiles(10, 8, 800.0, 600.0)
229
+ assert small.x1 == 9
230
+ assert small.y1 == 7
231
+ far := Camera(x: 3000.0, y: 3000.0, scale: 1.0).visibleTiles(64, 64, 800.0, 600.0)
232
+ assert far.x1 == 63
233
+ assert far.y1 == 63
234
+
235
+ test "clampTo keeps the view in the map and centres a small map"
236
+ c := Camera(x: -50.0, y: 5000.0, scale: 1.0)
237
+ c.clampTo(2048.0, 2048.0, 800.0, 600.0)
238
+ assert c.x == 0.0
239
+ assert c.y == 1448.0
240
+ c.x = 5000.0
241
+ c.clampTo(2048.0, 2048.0, 800.0, 600.0)
242
+ assert c.x == 1248.0
243
+ s := Camera(x: 99.0, y: 99.0, scale: 1.0)
244
+ s.clampTo(320.0, 240.0, 800.0, 600.0)
245
+ assert s.x == -240.0
246
+ assert s.y == -180.0
247
+
248
+ test "a minimap click centres the camera"
249
+ m := miniRect(800.0, 600.0, 64, 64)
250
+ assert m.cell == 2.5
251
+ assert m.x == 632.0
252
+ assert m.y == 432.0
253
+ w := miniToWorld(m, 64, 64, Vec2(x: 632.0 + 25.0, y: 432.0 + 50.0)) ?: Vec2.Zero
254
+ assert w == Vec2(x: 320.0, y: 640.0)
255
+ assert miniToWorld(m, 64, 64, Vec2(x: 10.0, y: 10.0)) == null
256
+ c := Camera(x: 0.0, y: 0.0, scale: 2.0)
257
+ c.centerOn(w, 800.0, 600.0)
258
+ assert c.screenToWorld(Vec2(x: 400.0, y: 300.0)) == w