@adatechnology/game-contracts 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +43 -2
- package/dist/index.global.js +21 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +21 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -835,6 +835,8 @@ type DestructionEvent = (EventBase & {
|
|
|
835
835
|
* One unit of a wall of units (`PartSpec.units`: a brick, a block) came out under shots or blows: `center` in world space, `size` in the part's own
|
|
836
836
|
* axes and `rotation` the part's world rotation (the box of the unit that left), `fragments` the unit itself. The part stays: draw the gap, do not hide it.
|
|
837
837
|
* With `torn`, nothing left the part but what a blow tore off its face (splinters of wood, lumps of MDF): the hole and the marks around it stay drawn.
|
|
838
|
+
* With `units`, a blast knocked many units out at once: each one's box (`center` in world space, `size` in the part's axes, turned by `rotation`), and
|
|
839
|
+
* `center` and `size` are the box around them all.
|
|
838
840
|
*/
|
|
839
841
|
| (EventBase & {
|
|
840
842
|
type: 'chip';
|
|
@@ -844,6 +846,10 @@ type DestructionEvent = (EventBase & {
|
|
|
844
846
|
rotation: Quat;
|
|
845
847
|
fragments: readonly Fragment[];
|
|
846
848
|
torn?: true;
|
|
849
|
+
units?: readonly {
|
|
850
|
+
center: Vec3;
|
|
851
|
+
size: Vec3;
|
|
852
|
+
}[];
|
|
847
853
|
})
|
|
848
854
|
/** Rebar of a reinforced part is bare: segments in world space. Empty `bars` means the cage is gone. */
|
|
849
855
|
| (EventBase & {
|
|
@@ -990,7 +996,7 @@ interface BlastShove {
|
|
|
990
996
|
}
|
|
991
997
|
/**
|
|
992
998
|
* The shove a blast gives to a body of `mass` kg whose centre is at `position`: away from the centre, lifted a little, with the speed
|
|
993
|
-
* `min(maxSpeed, sqrt(2 * energy *
|
|
999
|
+
* `sqrt(reach) * min(maxSpeed, sqrt(2 * energy * coupling / mass)) * (1 - distance / radius) ^ 2`. Undefined when the body is outside the radius or the speed is under `minSpeed`.
|
|
994
1000
|
* The same rule moves the loose items and furniture of the test room and the people of the characters module, so a game that tunes the options tunes all of them.
|
|
995
1001
|
*/
|
|
996
1002
|
declare function blastShove(blast: BlastSource, target: {
|
|
@@ -1075,6 +1081,17 @@ declare function stepKnock(knock: Knock, position: {
|
|
|
1075
1081
|
y: number;
|
|
1076
1082
|
z: number;
|
|
1077
1083
|
}, dt: number, groundAt: (x: number, z: number) => number, alive: boolean): boolean;
|
|
1084
|
+
/**
|
|
1085
|
+
* Throws a body at `velocity` (m/s, world): an occupant leaving a car through the glass, a pedestrian run over. The module that animates the body
|
|
1086
|
+
* takes it on its next frame (`takeLaunch`) and moves it as it moves a body a blast knocked down: flying, landing, sliding, lying, getting up.
|
|
1087
|
+
*/
|
|
1088
|
+
declare function launchBody(root: {
|
|
1089
|
+
userData: Record<string, unknown>;
|
|
1090
|
+
}, velocity: Vec3): void;
|
|
1091
|
+
/** The throw `launchBody` left on the root, as a knock (knocked down, in the air), once; undefined when nothing threw it. */
|
|
1092
|
+
declare function takeLaunch(root: {
|
|
1093
|
+
userData: Record<string, unknown>;
|
|
1094
|
+
}): Knock | undefined;
|
|
1078
1095
|
|
|
1079
1096
|
interface CostOptions {
|
|
1080
1097
|
/** Reads of `state()` and of `describe()` to time. Default 10 000. */
|
|
@@ -1550,6 +1567,16 @@ interface HandTargets {
|
|
|
1550
1567
|
}
|
|
1551
1568
|
/** Read every frame by whoever draws the arms; answers null while nothing is held. */
|
|
1552
1569
|
type HandTargetSource = () => HandTargets | null;
|
|
1570
|
+
/** The muzzle of the tool in the hands as it is drawn now (world space): where a shot's flash and tracer leave, and which way the barrel points. */
|
|
1571
|
+
interface MuzzlePose {
|
|
1572
|
+
readonly position: Vec3;
|
|
1573
|
+
/** Unit vector along the barrel, out of the muzzle. */
|
|
1574
|
+
readonly direction: Vec3;
|
|
1575
|
+
/** The module draws the muzzle flash itself: the host draws none of its own. */
|
|
1576
|
+
readonly drawsFlash: boolean;
|
|
1577
|
+
}
|
|
1578
|
+
/** Read by the host at each shot; null while the tool in the hands has no muzzle (a knife, a bare hand). */
|
|
1579
|
+
type MuzzleSource = () => MuzzlePose | null;
|
|
1553
1580
|
|
|
1554
1581
|
/** How a hand turns something round in strokes (a screw cap, a tap handle, a knob): what `twistStroke` answers for one moment of the turning. */
|
|
1555
1582
|
interface TwistStroke {
|
|
@@ -1896,6 +1923,11 @@ interface AudioSink {
|
|
|
1896
1923
|
setListener?(listener: AudioListener): void;
|
|
1897
1924
|
/** Adds a sound bank to the host's engine. `bank` is the engine's own type, `unknown` here because the contracts do not know it. */
|
|
1898
1925
|
register?(bank: unknown): void;
|
|
1926
|
+
/**
|
|
1927
|
+
* Fetches and decodes the recorded files of these sounds now, so their first play is the recording and not the synthesized fallback (a door
|
|
1928
|
+
* opened the moment the area loads). A host without recordings leaves it out; ids without files are ignored.
|
|
1929
|
+
*/
|
|
1930
|
+
preload?(sounds: readonly string[]): void;
|
|
1899
1931
|
}
|
|
1900
1932
|
|
|
1901
1933
|
/** The weather, light and ground as the world is right now. Written by whoever drives the weather, read by whoever reacts to it. */
|
|
@@ -2197,6 +2229,8 @@ interface AreaPlacement {
|
|
|
2197
2229
|
interface GroundZone {
|
|
2198
2230
|
readonly rect: readonly [number, number, number, number];
|
|
2199
2231
|
readonly surface: string;
|
|
2232
|
+
/** Sign a renderer lays on the front edge of the zone (the side of +z). */
|
|
2233
|
+
readonly label?: string;
|
|
2200
2234
|
}
|
|
2201
2235
|
/** What the floor is made of. `surface` is a ground profile id (`lawn`, `road`, `concrete_slab`, `tile_floor`...); a host that does not know it uses its default. */
|
|
2202
2236
|
interface GroundLayout {
|
|
@@ -2557,6 +2591,11 @@ interface ModuleHost {
|
|
|
2557
2591
|
* frame hooks have run, and puts the hands there. Pass null (or call the returned function) to let go. A host without arms leaves it out.
|
|
2558
2592
|
*/
|
|
2559
2593
|
setHandTargets?(source: HandTargetSource | null): () => void;
|
|
2594
|
+
/**
|
|
2595
|
+
* Lends the host the muzzle of the tool the module draws in the hands: the host reads `source` at each shot and puts the tracer (and, unless the module draws one, the flash) there, at the
|
|
2596
|
+
* muzzle of the model and not at a guess in front of the eye. Pass null (or call the returned function) to let go. A host that draws no shot effects leaves it out.
|
|
2597
|
+
*/
|
|
2598
|
+
setMuzzleSource?(source: MuzzleSource | null): () => void;
|
|
2560
2599
|
/**
|
|
2561
2600
|
* Where the player's body holds a tool now (world space), in every view (the weapon is the body's own): `position` is the pocket of the right shoulder a stock rests on, `quaternion` turns
|
|
2562
2601
|
* to the aim (-z the aim, x right, y up, as a camera). A module draws the tool there at its real size. Null for a host with no body (the module puts the same pocket under the camera's eye);
|
|
@@ -2585,6 +2624,8 @@ interface ModuleHost {
|
|
|
2585
2624
|
* the panels and the room's other keys keep working. A host without a walking player leaves it out.
|
|
2586
2625
|
*/
|
|
2587
2626
|
holdPlayer?(held: boolean): void;
|
|
2627
|
+
/** True while a module holds the player (`holdPlayer(true)`): the keys then belong to that module, and another module's own keys (a weapon's accessories) stay out of its way. */
|
|
2628
|
+
playerHeld?(): boolean;
|
|
2588
2629
|
/** Lent only to a module that declares controls, layers or metrics. */
|
|
2589
2630
|
bench?: ModuleBench;
|
|
2590
2631
|
/** The host's shared sound engine; a module that makes sound uses it when lent and stays silent (or brings its own) when not. */
|
|
@@ -2870,4 +2911,4 @@ declare function footprintBlocker(object: FootprintObject | (() => FootprintObje
|
|
|
2870
2911
|
/** @deprecated Import it from `@adatechnology/game-contracts/testing`. */
|
|
2871
2912
|
declare const resetWarnOnce: () => void;
|
|
2872
2913
|
|
|
2873
|
-
export { type ActionControl, type AnimationClipSpec, type AnimationEventSpec, type AnimationKey, type AnimationPose, type AreaPlacement, type AudioListener, type AudioSink, BLOW_IDS, BODY_FOLEY_IDS, BODY_SOUND_IDS, type BallisticExtras, type BenchChange, type BenchDeclaration, type BenchReport, type BenchSheet, type BlastImpulseOptions, type BlastOptions, type BlastReport, type BlastShove, type BlastShoveOptions, type BlastSource, type BlowId, type BlowTarget, type BodyFoleyId, type BodyRegion, type BodySlot, type BodySoundId, type BooleanControl, type BusyOptions, type BusySink, type BusyTask, type CategorizedOptions, type CategorizedReport, type CategoryDef, type CategoryOptions, type CategoryRegistry, type ChainDelay, type CheckOptions, type CheckSpecOptions, type ChoiceControl, type ChoiceOption, type ConfinementOptions, type ContainerSpec, type ContractChecker, type ContractContext, type ControlPatch, type ControlValue, type CostOptions, type Crack, DEFAULT_BLAST_IMPULSE, DEFAULT_BLAST_SHOVE, DEFAULT_ENVIRONMENT, DEFAULT_IGNITION_RULES, DEFAULT_SURFACE, DEFERRED_PRIORITY, DEFERRED_QUEUE_ID, type DamageStage, type DamageType, type DebrisKind, type DeepPartial, type DeferredQueue, type DeferredQueueOptions, type DeferredQueueReport, type Dent, type DestructibleImpl, type DestructibleSpec, type DestructionEvent, type DestructionEventType, type EffectHandle, type EffectRequest, type EffectSink, type Enclosure, type EnclosureBox, type EnvironmentBus, type EnvironmentKey, type EnvironmentState, type ExplosionChain, FOOTSTEP_SOUND_IDS, FRAGMENT_COVER, FRAME_PHASES, type FluidKind, type Footprint, type FootprintObject, type FootstepSoundId, type FractureStyle, type Fragment, type FragmentHitOptions, type FragmentMark, type FrameOptions, type FramePhase, GAIT_IDS, type GaitId, type GroundLayout, type GroundZone, HEAVY_LANDING_SPEED, type HandPose, type HandTargetSource, type HandTargets, type Hit, type HitIgnition, type HitResult, type Hole, type HostEvent, type HostGraphics, type HostReach, type HostTool, type HostToolAttachment, IMPACT_CONTRACT, type IgnitionRules, type ImpactContract, type ItemAnimationContract, type ItemSeat, type ItemSounds, type ItemUseDef, type JointKind, type JointSpec, type JointState, KIT_ORIGIN_KEY, KNOCKED_DOWN_ANGLE, KNOCK_CHEST, KNOCK_DOWN_SPEED, type KitOrigin, type Knock, LANDING_IDS, LAND_SOUND_IDS, type LandSoundId, type LandingId, type LiquidId, type LoopHandle, MATERIAL_SURFACES, MISSING_BITS, type MaterialId, type ModuleArea, type ModuleBench, type ModuleControl, type ModuleHost, type ModuleItemFluid, type ModuleItemSeal, type ModuleLayer, type ModuleMetric, type ModuleScenario, type ModuleView, type NumberControl, type ObjectState, type PartBreach, type PartShape, type PartSpec, type PartState, type PartStatus, type PartUnits, type PileSpec, type PlayerState, type Quat, type RayHit, type Reinforcement, STAGE_EVENT, SURFACE_IDS, SWING_LEAD_SECONDS, type SealBlock, type SealSpec, type SealState, type SegmentKind, type ShelterOptions, type Shot, type SoilLayer, type SoilProfile, type SoilState, type SoundHandle, type SoundRequest, type SpecReport, type StructureRole, type SurfaceId, type SurfaceLayer, type SurfaceRay, type SurfaceRayHit, TOOL_CLIP, TWIST_LIFT, type TensionRebar, type TestableItem, type TestableModule, type Thumbnail, type ToolAnimationStep, type ToolAnimator, type ToolClipId, type ToolClipPlayer, type ToolClipPlayerOptions, type ToolClipSet, type ToolCommand, type ToolCommandResult, type ToolKind, type ToolRig, type TunedMassDamper, type TwistOptions, type TwistStroke, type UnmarkedDrawing, type Use, type Vec3, type WarmUpProbes, type WindExposure, type WindState, applyPatches, blastBodySpeed, blastShove, bodyImpulse, bodyMass, checkAnimation, checkBench, checkCategorized, checkDeferredQueue, checkDestructible, checkHit, checkSpec, checkToolClips, createEnvironmentBus, createToolClipPlayer, deepMerge, defineCategories, fillFactor, footprintBlocker, footstepSoundId, hitSoundId, ignitionChance, isSurfaceId, kitOriginOf, knockOf, landSoundId, leadClipEvents, markKitOrigin, mergeById, removeIds, resetWarnOnce, resolveEntry, resolveIgnitionRules, sampleClip, scaleClip, sealBlock, sealOpener, shotExtras, shotToHit, sideOnImpulse, sparkTargetFactor, stepKnock, surfaceOfMaterial, swingSoundId, toolAnimationStep, twistStroke, unmarkedDrawings, useToHit };
|
|
2914
|
+
export { type ActionControl, type AnimationClipSpec, type AnimationEventSpec, type AnimationKey, type AnimationPose, type AreaPlacement, type AudioListener, type AudioSink, BLOW_IDS, BODY_FOLEY_IDS, BODY_SOUND_IDS, type BallisticExtras, type BenchChange, type BenchDeclaration, type BenchReport, type BenchSheet, type BlastImpulseOptions, type BlastOptions, type BlastReport, type BlastShove, type BlastShoveOptions, type BlastSource, type BlowId, type BlowTarget, type BodyFoleyId, type BodyRegion, type BodySlot, type BodySoundId, type BooleanControl, type BusyOptions, type BusySink, type BusyTask, type CategorizedOptions, type CategorizedReport, type CategoryDef, type CategoryOptions, type CategoryRegistry, type ChainDelay, type CheckOptions, type CheckSpecOptions, type ChoiceControl, type ChoiceOption, type ConfinementOptions, type ContainerSpec, type ContractChecker, type ContractContext, type ControlPatch, type ControlValue, type CostOptions, type Crack, DEFAULT_BLAST_IMPULSE, DEFAULT_BLAST_SHOVE, DEFAULT_ENVIRONMENT, DEFAULT_IGNITION_RULES, DEFAULT_SURFACE, DEFERRED_PRIORITY, DEFERRED_QUEUE_ID, type DamageStage, type DamageType, type DebrisKind, type DeepPartial, type DeferredQueue, type DeferredQueueOptions, type DeferredQueueReport, type Dent, type DestructibleImpl, type DestructibleSpec, type DestructionEvent, type DestructionEventType, type EffectHandle, type EffectRequest, type EffectSink, type Enclosure, type EnclosureBox, type EnvironmentBus, type EnvironmentKey, type EnvironmentState, type ExplosionChain, FOOTSTEP_SOUND_IDS, FRAGMENT_COVER, FRAME_PHASES, type FluidKind, type Footprint, type FootprintObject, type FootstepSoundId, type FractureStyle, type Fragment, type FragmentHitOptions, type FragmentMark, type FrameOptions, type FramePhase, GAIT_IDS, type GaitId, type GroundLayout, type GroundZone, HEAVY_LANDING_SPEED, type HandPose, type HandTargetSource, type HandTargets, type Hit, type HitIgnition, type HitResult, type Hole, type HostEvent, type HostGraphics, type HostReach, type HostTool, type HostToolAttachment, IMPACT_CONTRACT, type IgnitionRules, type ImpactContract, type ItemAnimationContract, type ItemSeat, type ItemSounds, type ItemUseDef, type JointKind, type JointSpec, type JointState, KIT_ORIGIN_KEY, KNOCKED_DOWN_ANGLE, KNOCK_CHEST, KNOCK_DOWN_SPEED, type KitOrigin, type Knock, LANDING_IDS, LAND_SOUND_IDS, type LandSoundId, type LandingId, type LiquidId, type LoopHandle, MATERIAL_SURFACES, MISSING_BITS, type MaterialId, type ModuleArea, type ModuleBench, type ModuleControl, type ModuleHost, type ModuleItemFluid, type ModuleItemSeal, type ModuleLayer, type ModuleMetric, type ModuleScenario, type ModuleView, type MuzzlePose, type MuzzleSource, type NumberControl, type ObjectState, type PartBreach, type PartShape, type PartSpec, type PartState, type PartStatus, type PartUnits, type PileSpec, type PlayerState, type Quat, type RayHit, type Reinforcement, STAGE_EVENT, SURFACE_IDS, SWING_LEAD_SECONDS, type SealBlock, type SealSpec, type SealState, type SegmentKind, type ShelterOptions, type Shot, type SoilLayer, type SoilProfile, type SoilState, type SoundHandle, type SoundRequest, type SpecReport, type StructureRole, type SurfaceId, type SurfaceLayer, type SurfaceRay, type SurfaceRayHit, TOOL_CLIP, TWIST_LIFT, type TensionRebar, type TestableItem, type TestableModule, type Thumbnail, type ToolAnimationStep, type ToolAnimator, type ToolClipId, type ToolClipPlayer, type ToolClipPlayerOptions, type ToolClipSet, type ToolCommand, type ToolCommandResult, type ToolKind, type ToolRig, type TunedMassDamper, type TwistOptions, type TwistStroke, type UnmarkedDrawing, type Use, type Vec3, type WarmUpProbes, type WindExposure, type WindState, applyPatches, blastBodySpeed, blastShove, bodyImpulse, bodyMass, checkAnimation, checkBench, checkCategorized, checkDeferredQueue, checkDestructible, checkHit, checkSpec, checkToolClips, createEnvironmentBus, createToolClipPlayer, deepMerge, defineCategories, fillFactor, footprintBlocker, footstepSoundId, hitSoundId, ignitionChance, isSurfaceId, kitOriginOf, knockOf, landSoundId, launchBody, leadClipEvents, markKitOrigin, mergeById, removeIds, resetWarnOnce, resolveEntry, resolveIgnitionRules, sampleClip, scaleClip, sealBlock, sealOpener, shotExtras, shotToHit, sideOnImpulse, sparkTargetFactor, stepKnock, surfaceOfMaterial, swingSoundId, takeLaunch, toolAnimationStep, twistStroke, unmarkedDrawings, useToHit };
|
package/dist/index.global.js
CHANGED
|
@@ -76,6 +76,7 @@ var GameContracts = (() => {
|
|
|
76
76
|
kitOriginOf: () => kitOriginOf,
|
|
77
77
|
knockOf: () => knockOf,
|
|
78
78
|
landSoundId: () => landSoundId,
|
|
79
|
+
launchBody: () => launchBody,
|
|
79
80
|
leadClipEvents: () => leadClipEvents,
|
|
80
81
|
markKitOrigin: () => markKitOrigin,
|
|
81
82
|
mergeById: () => mergeById,
|
|
@@ -94,6 +95,7 @@ var GameContracts = (() => {
|
|
|
94
95
|
stepKnock: () => stepKnock,
|
|
95
96
|
surfaceOfMaterial: () => surfaceOfMaterial,
|
|
96
97
|
swingSoundId: () => swingSoundId,
|
|
98
|
+
takeLaunch: () => takeLaunch,
|
|
97
99
|
toolAnimationStep: () => toolAnimationStep,
|
|
98
100
|
twistStroke: () => twistStroke,
|
|
99
101
|
unmarkedDrawings: () => unmarkedDrawings,
|
|
@@ -103,6 +105,7 @@ var GameContracts = (() => {
|
|
|
103
105
|
|
|
104
106
|
// src/blast-shove.ts
|
|
105
107
|
var DEFAULT_BLAST_SHOVE = { coupling: 0.02, maxSpeed: 20, lift: 0.3, minSpeed: 0.3, falloffPower: 2 };
|
|
108
|
+
var clampShare = (share) => Math.min(1, Math.max(0, share));
|
|
106
109
|
function blastShove(blast, target, options = {}) {
|
|
107
110
|
const o = { ...DEFAULT_BLAST_SHOVE, ...options };
|
|
108
111
|
const dx = target.position[0] - blast.point[0];
|
|
@@ -112,7 +115,8 @@ var GameContracts = (() => {
|
|
|
112
115
|
if (!(blast.radius > 0) || distance > blast.radius) return void 0;
|
|
113
116
|
const falloff = (1 - distance / blast.radius) ** o.falloffPower;
|
|
114
117
|
const mass = Math.max(target.mass, 0.05);
|
|
115
|
-
const
|
|
118
|
+
const reach = clampShare(blast.reach ?? 1);
|
|
119
|
+
const speed = Math.sqrt(reach) * Math.min(o.maxSpeed, Math.sqrt(2 * blast.energy * o.coupling / mass)) * falloff;
|
|
116
120
|
if (!(speed >= o.minSpeed)) return void 0;
|
|
117
121
|
const direction = distance > 1e-4 ? [dx / distance, dy / distance + o.lift, dz / distance] : [0, 1, 0];
|
|
118
122
|
const length = Math.hypot(direction[0], direction[1], direction[2]);
|
|
@@ -218,6 +222,16 @@ var GameContracts = (() => {
|
|
|
218
222
|
} else if (knock.lying > 0) knock.lying = Math.max(0, knock.lying - dt / RISE_SECONDS);
|
|
219
223
|
return !moving && !knock.down && knock.lying === 0;
|
|
220
224
|
}
|
|
225
|
+
var LAUNCHED = "launched";
|
|
226
|
+
function launchBody(root, velocity) {
|
|
227
|
+
root.userData[LAUNCHED] = [velocity[0], velocity[1], velocity[2]];
|
|
228
|
+
}
|
|
229
|
+
function takeLaunch(root) {
|
|
230
|
+
const velocity = root.userData[LAUNCHED];
|
|
231
|
+
if (!velocity) return void 0;
|
|
232
|
+
delete root.userData[LAUNCHED];
|
|
233
|
+
return { velocity: [velocity[0], velocity[1], velocity[2]], airborne: true, lying: 0, lieLeft: 0, down: true };
|
|
234
|
+
}
|
|
221
235
|
|
|
222
236
|
// src/impact-cost.ts
|
|
223
237
|
var DEFAULT_COST_READS = 1e4;
|
|
@@ -1242,6 +1256,8 @@ var GameContracts = (() => {
|
|
|
1242
1256
|
mortar: "concrete",
|
|
1243
1257
|
stone: "concrete",
|
|
1244
1258
|
brick: "concrete",
|
|
1259
|
+
cobble: "concrete",
|
|
1260
|
+
paver: "concrete",
|
|
1245
1261
|
plaster: "concrete",
|
|
1246
1262
|
asphalt: "asphalt",
|
|
1247
1263
|
wood_soft: "wood",
|
|
@@ -1252,6 +1268,8 @@ var GameContracts = (() => {
|
|
|
1252
1268
|
ceramic: "tile",
|
|
1253
1269
|
plastic: "tile",
|
|
1254
1270
|
soil: "dirt",
|
|
1271
|
+
mud: "dirt",
|
|
1272
|
+
clay: "dirt",
|
|
1255
1273
|
wax: "dirt",
|
|
1256
1274
|
flesh_fruit: "dirt",
|
|
1257
1275
|
turf: "grass",
|
|
@@ -1259,6 +1277,8 @@ var GameContracts = (() => {
|
|
|
1259
1277
|
cactus: "grass",
|
|
1260
1278
|
gravel: "gravel",
|
|
1261
1279
|
sand: "sand",
|
|
1280
|
+
// no snow surface yet: packed snow crunches closest to sand underfoot
|
|
1281
|
+
snow: "sand",
|
|
1262
1282
|
steel: "metal",
|
|
1263
1283
|
cast_iron: "metal",
|
|
1264
1284
|
aluminium: "metal",
|