Giter Site home page Giter Site logo

vpdb / vpx-js Goto Github PK

View Code? Open in Web Editor NEW
49.0 4.0 11.0 23.22 MB

:video_game: Visual Pinball in the Browser

License: GNU General Public License v2.0

TypeScript 94.09% JavaScript 0.46% VBScript 5.45%
pinball visualpinball gltf pinball-simulation physics-engine javascript

vpx-js's Introduction

Visual Pinball X in JavaScript

A port of the best pinball simulator out there

Build Status codecov Dependencies

Features

This isn't a ready-to-use game. It's a library of loosely-coupled components that together implement Visual Pinball's player for the web.

The player can be split into three parts:

  1. The rendering engine
  2. The physics engine
  3. The scripting engine

This library provides an abstraction layer for rendering with three.js, which covers the first point. A physics loop is implemented by the Player class. Collision detection and rigid body dynamics are fully ported, covering the second part. Work on scripting has begun with the wiring set up and the default table script working. More info about how we go about this can be found here.

Rendering

VPX-JS reads Visual Pinball's VPX format and extracts all meshes in VP's internal format. Using an abstraction layer, any WebGL framework can convert this format and construct a scene. An adapter for three.js is included.

Additionally, VPX-JS supports direct export to GLTF files, which is nice, because it allows off-loading the export to a server. It's also nice because GLTF allows doing stuff that Visual Pinball's OBJ export doesn't, for example:

  • Include materials, textures and lights in one single file
  • Apply optimizations:
    • PNG textures with no transparency are converted to JPEG
    • PNG textures with transparency are PNG-crushed
    • Compress meshes with Draco

image A table in the browser using three.js

Physics

VPX-JS uses the same physics code than Visual Pinball. That means the gameplay is identical in the browser than when running VPX.

Scripting and VPM

For scripting, see this issue. About VPM, there isn't a JavaScript implementation of PinMAME yet. However, @neophob wrote a WPC emulator from scratch that will cover many games already.

Development Setup

Given this is a lib, you'll need an actual web application to test. There is a simple one we're currently using for development here.

This vpweb project retrieves VPX-JS from NPM, so in order to iterate rapidly, we'll link it to your local working copy.

git clone https://github.com/vpdb/vpx-js.git
cd vpx-js
npm ci
npm link
npm run build:watch

And the vpweb host application:

git clone https://github.com/freezy/vpweb.git
cd vpweb
npm ci
npm link vpx-js
npm start

Then connect to http://localhost:8080 and drag a VPX file into it. Note that the scripting engine is still limited. However, the table script of the default table should now work.

Usage

WIP. The API will be documented when it's considered stable.

Tests

Run tests with:

npm run test

For more infos about how tests are written, see here

Credits

  • @jsm174 for getting the Nearley grammar right and his work on translating VBScript to JavaScript
  • @neophob for his awesome WPC-EMU integration

IntelliJ IDEA

Special thanks go to JetBrains for their awesome IDE and support of the Open Source Community!

License

GPLv2, see LICENSE.

vpx-js's People

Contributors

dependabot[bot] avatar freezy avatar jsm174 avatar neophob avatar renovate-bot avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

vpx-js's Issues

Inline If Statement parse failure

The following VBScript currently can not be parsed:

If VPMver > "" Then If Controller.Version < VPMver Or Err Then MsgBox "VPinMAME ver " & VPMver & " required."

Handle playfield mesh properly

Currently we generate a block for the playfield (the board) based on the table's dimensions. Visual Pinball only generates a plane, but it also allows creating a primitive that acts as playfield. The latter needs proper support in VPX since currently it would generate both the block and the primitive in case a primitive is used.

Implement VBS Standard Library

VBScript obviously comes with a standard lib. This is probably one of the last tasks of the scripting engine. The goal is not to have a 100% covered stdlib, but to analyze the table scripts and check which functions and classes are used.

Wire dipswitch setting to WPC-Emu

Dip calls are currently not forwarded to wpc-emu. those settings are used to define country settings - so they should be routed.

Examples:

Fish Tales 		.DIP(0) = &H80
WhiteWater	        Controller.DIP(0) = &H00
TAFG                        .dip(0)=&h00
NBA                         Controller.DIP(0) = &H00

-> for WPC games, the offset of the call does not matter

Deploy vpweb

goal is to have the latest version of vpx-js and vpweb running on pinballtube.com as a start.

@freezy will do some cleanup first, after that the goal is that we have an automated deployment using github actions

Add remaining events

The most important events are already covered, but there are more. This is essential for the scripting engine to work.

Cancel Loading a Table

VPWeb can close the current table already - nice to have would be, if we can cancel the loading process of a table (and jump back to the main menu).

If loading a table fails (like due vbs issues) the app is stuck, there's no way to go back

Implement Global Sets

In Visual Pinball, under Preferences / Configure (Optional) Global Physics Sets, you can define up to nine "Sets", which are flipper and playfield settings that will override those defined in the table. Sets can also be imported and exported.

In the code, these show up as override* properties of the data object. For example, the FlipperData has those props:

public overrideMass?: number;
public overrideStrength?: number;
public overrideElasticity?: number;
public overrideElasticityFalloff?: number;
public overrideFriction?: number;
public overrideReturnStrength?: number;
public overrideCoilRampUp?: number;
public overrideTorqueDamping?: number;
public overrideTorqueDampingAngle?: number;
public overrideScatterAngle?: number;
public overridePhysics?: number;

Those would be set from the global sets. Currently they are undefined.

This is probably low priority.

Handle VBScript errors

One of VBScript's most embarrassing parts is its error handling. Basically you can turn it on and off at any time, at runtime. Error handling by side-effect, yay.

In JavaScript, there is no such thing. You can define a global error handler, but that won't resume code execution. For VBScript's API, that's less of a problem because we can handle errors ourselves (i.e. set the Err object accordingly), but for language features this doesn't work.

The first time we encounter this problem is how core.vbs updates lamps:

On Error Resume Next
	For ii = 0 To UBound(ChgLamp)
		idx = ChgLamp(ii, 0)
		Lights(idx).State = ChgLamp(ii, 1)
	Next
On Error Goto 0

If the ROM returns a lamp index that doesn't exist on the playfield, our transpiled script currently crashes, because Lights[idx] returns undefined, and undefined.State throws an error.

In VBScript this works, because error handling muted by On Error Resume Next. If Lights(idx) doesn't exist, well, never mind and continue.

There are a few ways of handling that, none particularly elegant.

Wrap every statement into a function

Since whether throw or swallow errors is a runtime condition, we can't determine at compile time which statements to wrap into a function that doesn't throw in case errors are muted. That means wrapping every statement of the table script into a function.

Apart from the code becoming an unreadable mess, this is also pretty bad for performance. Take the following snippet:

Click to view
function wrap(fct) {
	try {
		fct();
	} catch {
		// no nothing
	}
}

const ni = 1e9;
const o = {};

const time1 = Date.now();
for (let i = 0; i < ni; i++) {
	o.test = i;
}
const dTime1 = Date.now() - time1;

const time2 = Date.now();
for (let i = 0; i < ni; i++) {
	wrap(() => o.test = i);
}
const dTime2 = Date.now() - time2;

console.log('before: %sms', dTime1);
console.log('after:  %sms', dTime2);
Results with one billion iterations:
before after
Node.js 12.8, Win64 551ms 3064ms
Chrome 78, Win64 545ms 2349ms
Chrome Canary 80 602ms 3621ms

So this adds factor three to six. Which is pretty bad. Maybe there are sufficiently little statements in an average table script to stays below budget, but I doubt that.

Wrap only language features

We could start by wrapping only member assignments, until the next road block. At some point we might need to be able to catch division by zero as well and end up like the first solutions, but it would be a more lightweight approach.

Concretely, Lights(idx).State = ChgLamp(ii, 1) would compile to __vbsHelper.set(Lights[idx], 'State', ChgLamp[ii][1]), and the set method would only fail if error handling is enabled.

Fix this specific case by returning fake-lights

Our table could simply return 200 lights like core.vbs loops over that do nothing, and this particular case would be solved. It comes with a (memory) overhead as well, and doesn't solve the actual problem.

Use a core.vbs that doesn't rely on On Error

We can also fix this at the source, at core.vbs. We've got our hands on it, since we bundle it. This would however mean that as core.vbs evolves in the future, we need to re-apply our patches (does happen a few times a year). It also doesn't solve the problem for in-table shipped scripts.

Conclusion

I would reevaluate this once we get core.vbs working. If there are no other cases, we might as well use the third approach. If there are more, but only for undefined properties, the second approach might be better. The first one, while the "most proper" one, should be avoided, given the performance hit.

Provide initialised RAM / EMU state

If the emulator is initialised, the RAM is not initialised - which results in a special message of the WPC ROM that needs to be confirmed with the ESC key. Today we work around this by:

				//TODO HACK - used to launch the rom - if we have a RAM state, this would be obsolete!
				setTimeout(() => {
					logger().info('ESC!');
					emulator.setCabinetInput(16);
				}, 2000);

but this is definitive a hack. we should provide a default RAM state that is downloaded and applied to avoid this

Handle textures in the browser

Seems like three.js needs a HTMLImageElement when assigning a map. Since we don't have an URL to load the data, we have two possibilities:

  1. Instantiate HTMLImageElement and serialize the image into a data URL. Seems like an unnecessary overhead.
  2. Use our own implementation of HTMLImageElement. This will only work if the image blob is somehow extracted before sending it to the WebGL API.

Then we'll need to convert all of Visual Pinball's formats into something supported by the browser. I think .bmp is the only low-dynamic format we'll need to convert, but there is also .exr and .hdr support, which might need conversion too.

In case a lot of conversion is going on, we might need to off-load this to a web worker for better performance.

Remaining Scripting Tasks

  • Multiple Statements in one line using colons are not treated like block statements:

Currently:

If Err Then MsgBox "Can't open ""core.vbs""" : Exit Sub

renders:

    if (Err)
        MsgBox('Can\'t open "core.vbs"');
    return;

This was fixed in scripting/ebnf and merged into master.


  • Check how OR should be rendered.

Currently:

If VPBuildVersion < 0 Or Err Then
   x = 5 
End If

renders:

if (VPBuildVersion < 0 | Err) {
    x = 5;
}

This was fixed in scripting/ebnf as a logical or expression (||) and merged into master.


  • Implement classes and properties: ClassDecl, MemberDeclList, MemberDecl, PropertyDecl, and PropertyAccessType

JavaScript has classes but does not support fields.

Classes were implemented in scripting/ebnf and merged into master. ProperyDecl's were implemented as methods.


  • Properly support array vs call. For example:
   Controller.Switch(swLRFlip) = True

should probably render in JavaScript:

   Controller.Switch[swLRFlip] = true;

Do we need to make a second pass, to grab a collection of all variables in relation to their position on the stack. If we find a subcall that matches a variable name, switch statement to array

This was implemented in scripting/ebnf and merged into master.
result(0, 0, 0) = 42 --> _scope.result[0][0][0] = 42


  • Replace Id that might match JavaScript keywords.

For example, does Switch cause issues?

You could do a second pass, and all identifiers that match keywords, auto append something

or

maybe it can altered directly in the id helper post processor?

No updates were made as this does not seem to be an issue.


  • Correctly implement ByRef & ByVal.

Currently these are just ignored. In JavaScript, only objects are passed by reference


  • Redim needs to be fixed. Redim does not first require a Dim. Should we make a second pass and look for any previous Dim or additional Redim and change to assignment statements.

Currently:

Redim x(5, 5) 
Redim Preserve x(5, 10) 

renders:

let x = vbsHelper.redim(x, [
    5,
    5
]);
let x = vbsHelper.redim(x, [
    5,
    10
], true);

And would fail with:

Identifier 'x' has already been declared

This was implemented in scripting/ebnf and merged into master.
redim is no longer implemented as variable declaration.


  • Fields need to be really implemented Default, Erase, Error, Explicit, Step, Private, Public

These were implemented in scripting/ebnf and merged into master.
Step is a keyword in Visual Basic, but not VBScript. Option Explicit is ignored and only allowed as first line in program. Erase has been implemented as a dim`


  • Error statements need to be really implemented

Currently they are just rendered as comments:

;    // On Error Resume Next

**This was implemented in scripting/ebnf and merged into master.


  • Special character will break the parser

Currently WPC.vbs contains 0x93 and 0x94 in a comment:

'Everytime you press the flipper keys those switches are <93>on<94>,

This was implemented in scripting/ebnf and merged into master. The parser is now based on node-ebnf. The program is first parsed into tokens (as per microsoft docs), formatted, and then standardized.


  • Sometimes whitespace will lock up the parser

Example:

a.b(1).c(2) (3).d(4, 5)

should render as:

a.b(1).c(2, 3).d(4, 5)

This was implemented in scripting/ebnf and merged into master. Related to above, after the script if formatted and standardized, no extra whitespace is present.


  • Currently implement Nothing vs Null vs Empty literals.

Currently all are rendered as null

This was implemented in scripting/ebnf and merged into master. Nothing, Null, and Empty now have __stdlib members that represent null and undefined.


  • Implement 3 remaining SubCallStmt in grammar, and determine how SubSafeExprOpt is meant to be used
                     | QualifiedID _ IndexOrParamsList %dot LeftExprTail _ SubSafeExprOpt _ CommaExprList                                {% ppSubCall.stmt6 %}
                     | QualifiedID _ IndexOrParamsListDot LeftExprTail _ SubSafeExprOpt _ CommaExprList                                  {% ppSubCall.stmt7 %}
                     | QualifiedID _ IndexOrParamsList %dot LeftExprTail _ SubSafeExprOpt                                                {% ppSubCall.stmt8 %}
                     | QualifiedID _ IndexOrParamsListDot LeftExprTail _ SubSafeExprOpt                                                  {% ppSubCall.stmt9 %}

Need to confirm, but I believe IndexOrParamsListDot vs IndexOrParamsList %dot was intended to allow for whitespace a.b(1). c(2, 3)

This was fixed in scripting/ebnf and merged into master. Since the parser is now based on EBNF and microsoft specs, this is no longer relevant.


  • Finish LeftExpr and LeftExprTail

These are required for multiple object subcalls:

ExecuteGlobal fso.OpenTextFile("VPMKeys.vbs", 1).ReadAll

that renders as:

ExecuteGlobal(fso.OpenTextFile('VPMKeys.vbs', 1).ReadAll); 

ie, should ReadAll be ReadAll()?

This was fixed in scripting/ebnf and merged into master. Since the parser is now based on EBNF and microsoft specs, this is no longer relevant. See InvocationExpression and InvocationMemberAccessExpression


  • Implement CallStmt in grammar

This was fixed in scripting/ebnf and merged into master.


  • Fix Date literal. This may require a vbsHelper function because according to

http://www.herongyang.com/VBScript/Data-Type-Date-and-Time-Data-Literal.html

You can just do a time portion: #21:26:00#

**This was fixed in scripting/ebnf and merged into master. Since the parser is now based on microsoft specs, the date grammar is accurate. **


  • Determine what KeywordID and SafeKeywordID in the Rosetta Code grammar is trying to do

This was fixed in scripting/ebnf and merged into master. Since the parser is now based on EBNF and microsoft specs, this is no longer relevant.


  • Implement Imp logical implication function. This will need to be done as a vbsHelper as javascript does not have this built in:

https://www.promotic.eu/en/pmdoc/ScriptLangs/VBScript/Operat/Imp.htm


  • Find test cases for the following post processor helpers:
arg1
leftExpr1
leftExprTail1
qualifiedId2
indexOrParamsDot2
indexOrParamsDot4

This was fixed in scripting/ebnf and merged into master. Since the parser is now based on EBNF and microsoft specs, this is no longer relevant.


  • Add a test case for white space line continuation
vpmSystemHelp = "Zaccaria keys:" & vbNewLine &_
  vpmKeyName(keyInsertCoin1) & vbTab & "Insert Coin #1"   & vbNewLine &_
  vpmKeyName(keyInsertCoin2) & vbTab & "Insert Coin #2"   & vbNewLine &_
  vpmKeyName(keySelfTest)    & vbTab & "Open Coin Door"   & vbNewLine &_
  vpmKeyName(keyAdvance)     & vbTab & "Advance Test"

renders as:

vpmSystemHelp = 'Zaccaria keys:' + vbNewLine + vpmKeyName(keyInsertCoin1) + vbTab + 'Insert Coin #1' + vbNewLine + vpmKeyName(keyInsertCoin2) + vbTab + 'Insert Coin #2' + vbNewLine + vpmKeyName(keySelfTest) + vbTab + 'Open Coin Door' + vbNewLine + vpmKeyName(keyAdvance) + vbTab + 'Advance Test';

This was fixed in scripting/ebnf and merged into master. Since the parser first formats the script all line continuations merged into one line.


  • Check what NOT does on integer literals.

Do we need to implement ~ in certain cases?

This was fixed in scripting/ebnf and merged into master. Not was implemented as !


  • Fix lockup during the following:
If aNo < mSize Then mKick(aNo + 1).Kick 0, 0

This was fixed in scripting/ebnf and merged into master. Since the parser is now EBNF based, this is no longer an issue.

VBS function calls and invalid scope

vpm-controller.ts (class) exposes this function:

	public Run() {
//whatever
	}

when this class is called, this is assigned to the function instead to the class. Bcktrace of this call:

    at Function.Run (http://localhost:8080/b5e7b3b55421.worker.js:9222:55)
    at VBSHelper.getOrCall (http://localhost:8080/b5e7b3b55421.worker.js:12642:33)
    at TableApi.eval (tablescript.js:17:25)
    at TableApi.emit (http://localhost:8080/b5e7b3b55421.worker.js:101716:5)

    getOrCall(obj, ...params) {
        if (typeof obj === 'function') {
            return obj.bind(obj)(...params); <<
        }
        for (const param of params) {
            obj = obj[param];
        }
        return obj;
    }

this is definitive correct for setter and getter calls, not sure if thats the case for all class methods.

Proposal to solve this:

  1. fix how we call the functions in the VBS Helper
  2. use pure functions in the vpm-controller.ts (no class) - however not sure what other functions VBS Helper is calling

Improve logging

In my PR @freezy add a comment:

I usually try to prefix logs with where it occurs: [Myclass.myMethod]: My message

Make sense - why not improve the whole logger, some ideas:

  • create a instance with its class name as prefix
  • use debug - dynamic enable debug logging for certain classes might help
  • future improvements, when public - send error logs to log aggregator to get realtime feedback

VPDB.io ROM fetching

To get the ROM for a specific VPX table, this is happening:

  • GameName property is set (Pinmame game name)
  • wpc-emu exposes a function that gives you the VPDB io of this game (if supported)
  • fetch ROM list from VPDB.io (https://api.vpdb.io/v1/games/${id}/roms) - where we can find the GameName in the id attribute
  • Using the rom_files key we see all the available roms:
		"rom_files": [
			{
				"filename": "mm_1_09c.bin",
				"bytes": 1048576,
				"crc": 3655669919,
				"modified_at": "1996-12-25T04:32:00.000Z"
			},
			{
				"filename": "mm_s2.1_0",
				"bytes": 524288,
				"crc": 3311156081,
				"modified_at": "1996-12-25T04:32:00.000Z"
			},
			{
				"filename": "mm_sav3.rom",
				"bytes": 1048576,
				"crc": 3978028400,
				"modified_at": "1996-12-25T04:32:00.000Z"
			},
			{
				"filename": "mm_sav4.rom",
				"bytes": 1048576,
				"crc": 2626284239,
				"modified_at": "1996-12-25T04:32:00.000Z"
			},
			{
				"filename": "mm_sav5.rom",
				"bytes": 1048576,
				"crc": 1158192688,
				"modified_at": "1996-12-25T04:32:00.000Z"
			},
			{
				"filename": "mm_sav6.rom",
				"bytes": 1048576,
				"crc": 1134384626,
				"modified_at": "1996-12-25T04:32:00.000Z"
			}
		],

the problem is - we don't have a clue what those files are used for. there is no default extension for the roms. I see two options to resolve this:

  • adding a new entry to the game entry that defines the main rom
  • the rom_files data structure gets a new "type" attribute: mainRom, soundRom, soundData, state, dump

Store maximal Lamps per ROM (EmulatorState)

Today this hack is in place:

		for (let n: number = 0; n < newState.length; n++) {
			// HACK: looks like we need the number of lamps per table (here 61)
			if (n < 62 && lastState[n] !== newState[n]) {
				const index = offsetMapperFunction(n);
				result.push([index, newState[n]]);
			}
		}

if the ChangedLamp returns more lamps (index > max lamps) then the VPX implementation chokes - so we need to know how many lamps each rom/game includes and apply this information when fetching nr of ChangedLamps. Probably we also need to know the nr of GI strings and solenoids

Integrate WPC-EMU

WPC-EMU is an awesome emulator for WPC ROMs, written in JavaScript.

Let's describe how the web application deals with VPX-JS and how WPC-EMU could fit in.

Initialization

When a table file is dropped into the browser, two things happen:

  1. The UI thread reads the VPX file and creates a three.js scene based on the geometry, textures, materials and lights in the file.
  2. A worker thread is spawned. The worker thread also reads the VPX file but ignores the geometry, textures and materials. Instead, it creates a collision tree, instantiates an object for every playfield element, transpiles and executes the table script, and then goes into a loop, where it stays indefinitely.

Gameplay

The UI thread is pretty dumb. Every frame it posts a message to the worker asking for the changed states since last time. When received, it applies it to the scene and goes on rendering until the next frame. It also sends key presses to the worker.

When the worker thread gets the state request, it has already simulated everything it needs to. So it just diffs the current state against the last sent state, sends it over, and continues its loop. It also receives key presses, which the table script knows how to deal with.

WPC-EMU

The emu is similar to the physics loop: It has a clock, and on every tick, it simulates the time that has been passed since the last tick.

This fits well into the current situation. The player on the worker thread, which already loops and simulates the physics, would additionally run a cycle on the emulator. Which means executing the executeCycle() method of the emu instance.

Note that the worker thread loop doesn't have a fixed-time duration. It essentially runs as fast as it can, since it has the whole thread at its disposal. That means if physics calculations get heavy, the duration will increase.

The API

Now, the table script uses an ActiveX COM Object to interface with PinMAME. Let's call it the VPM Controller. This object is instantiated in the table script, typically at the beginning, and is used to control the playfield and receive user and physics inputs.

So the VPM Controller is the class that would instantiate WPC-EMU and translate its API into PinMAME's.

There is just one problem: The table script doesn't know about the clock. It just assumes PinMAME does its thing all alone, which is the case for Visual PinMAME, running on its own thread.

There are two solutions for that:

  1. We make the player instance a static singleton and let the VPM controller grab it by itself
  2. We patch the script to pass the player instance to the controller

I'd vote for 2. So, basically, the VPM controller should look similar to this:

export class VpmController {

  private emu?: Emu;

  constructor(player: Player) {
    this.initStuff.then(wpcSystem => {
      this.emu = new Emu(wpcSystem);
      player.enableEmuCycle(this.emu);
    });
  }

  public ChangedLamps(): Array<Array<number>> {    
    if (!this.emu) { // might not have been initialized yet
      return [[]];
    }
    const changedLamps = diff(this.emu.getLamps(), this.lastLamps);
    this.lastLamps = this.emu.getLamps();
    return changedLamps;
  }   
  // rest of the API
}

Excuse my naive implementation of ChangedLamps, that's just to illustrate that all those methods will use the emu instance to do their thing.

Initial switch state (Fliptronics)

Today the wpc-system is not initialize the fliptronics or wpc-95 flipper switches. it looks like the visual pinball scripts do not initialize those swiches correctly.

Also WPC-Emu did the switch initialization on it's own.

@freezy do you agree that this needs to be done by the WPC-Emu or is there a section in the VBS script that we don't parse/execute right now?

To reproduce this issue:

  • Press the "Coin Door" button to "open" this pinball machine
  • Press "Enter" - there you see "R. FLIPPER E.O.S. IS STUCK CLOSED" and "L. FLIPPER E.O.S. IS STUCK CLOSED"

WPC-Emu cleanup round

#124 was about a first working WPC-Emu integration. there are some issues I want to address here:

DONE

  • use module.exports for wpc-emu lib, today the facade use the modules export. reason: some unit tests fail running in node
  • check architecture about the wpc-emu, its caching mechanism - maybe use a proxy?

PENDING

  • use run function to start the emulator, use stop function to stop the emulator

Implement Timers

Visual Pinball works a lot with timers. None of it is implemented.

Handle VBScript's case insensitivity

Still a few items on the list:

  • Handle global scope (handled at runtime, uses a ScopeHandler proxy)
  • Handle table item APIs (sorted out at compile time)
  • Handle enums (handled at compile time, using VbsApi)
  • Handle global API (compile time, also VbsApi)
  • Handle stdlib calls (compile time, also VbsApi)
  • Handle ActiveX COM implementations (#174)
  • Handle local scope

And probably more.

TODO

COM Implementations

I don't think they can be handled at compile time, since they are instances created by the table script we cannot query when compiling (table elements, enums etc work because we have them all at compile time).

So the getObject() method should return a ScopeHandler-like proxy that keeps an index when accessing props and methods and returns the correctly spelled version.

Local Scope

This should be doable at compile time, since all variables are declared in the same scope and don't come from other included files we know nothing about. I think I've tried this already but ascope choked at the AST we have when we do this.

Fix variable declarations

It looks like the VariableDeclarator in the AST is skipped. The order is:

VariableDeclaration -> VariableDeclarator -> Identifier

Example:

Dim EnableBallControl

results in:

{
  "type": "VariableDeclaration",
  "kind": "let",
  "declarations": [ {
    "type": "Identifier",
    "name": "EnableBallControl"
  } ]
}

where is should result in:

{
  "type": "VariableDeclaration",
  "kind": "let",
  "declarations": [ {
    "type": "VariableDeclarator",
    "id": {
      "type": "Identifier",            
      "name": "EnableBallControl"
    }
  } ]
}

The code generator doesn't seem to mind, but escope does, i.e. the declared variable won't show up in the scope.

THREE.WebGLRenderer: Texture marked for update but image is incomplete

I suspect that somehow we miss an async call in our code - which does not matter on fast machines but fails on my slower machine. Thats why I see tons of those warnings as defined in the title.

Code of threejs that produces this warning is here:

	function setTexture2D( texture, slot ) {

		var textureProperties = properties.get( texture );

		if ( texture.isVideoTexture ) updateVideoTexture( texture );

		if ( texture.version > 0 && textureProperties.__version !== texture.version ) {

			var image = texture.image;

			if ( image === undefined ) {

				console.warn( 'THREE.WebGLRenderer: Texture marked for update but image is undefined' );

			} else if ( image.complete === false ) {

				console.warn( 'THREE.WebGLRenderer: Texture marked for update but image is incomplete' );

			} else {

				uploadTexture( textureProperties, texture, slot );
				return;

			}

		}

		state.activeTexture( 33984 + slot );
		state.bindTexture( 3553, textureProperties.__webglTexture );

	}

It's not reliable reproducible - a fresh build (uncached?) seldom shows the error, a reload of the cached table most of the time.

Add API changes to states

Currently states only get data from the physics loop, i.e. physics based movements.

But there are other changes that affect the UI that can be triggered via script, e.g. texture switches, transformations, color, visibility etc etc.

The best way to determine which variables need to be moved to the state is to look at the render functions of Visual Pinball and check which members are used to draw the object.

Implement Collections

Collections in the script API are arrays that can be looped through. There is also some event handling that needs to be further examined. They are currently parsed and available in the table object but need get their proper API.

Error handling - worker thread -> main thread

Currently when the ROM download fails from vpdb.io, all we do is log an error on the console, which leave the user puzzled.

Goal is to have an error handler, that can send a message (including details) to the main thread, so at least the user is aware that there is something wrong

Implement primitive animations

I'm still not sure this is used anywhere, but primitives can somehow be animated. Search for TODO animation in the current code.

Implement Visual Pinball's VBScript API

Visual Pinball provides an API to the VBScript engine. This allows interaction between the script and the playfield elements, but also includes rather generic functions like PlaySound.

Here a list of APIs to implement. Those checked are already implemented and merged.

While control points do have an API, I have no idea how it is exposed. I guess we'll see when running first scripts through it.

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Warning

These dependencies are deprecated:

Datasource Name Replacement PR?
npm @types/sharp Unavailable
npm text-encoding Unavailable
npm tslint Unavailable

Rate-Limited

These updates are currently rate-limited. Click on a checkbox below to force their creation now.

  • chore(deps): update dependency @types/microtime to v2.1.2
  • chore(deps): update dependency @types/moo to v0.5.9
  • chore(deps): update dependency @types/nearley to v2.11.5
  • chore(deps): update dependency @types/sinon-chai to v3.2.12
  • chore(deps): update dependency buffer to v6.0.3
  • chore(deps): update dependency cross-env to v7.0.3
  • chore(deps): update dependency ebnf to v1.9.1
  • chore(deps): update dependency moo to v0.5.2
  • chore(deps): update dependency sinon to v9.2.4 (sinon, @types/sinon)
  • chore(deps): update dependency @types/chai to v4.3.17
  • chore(deps): update dependency @types/node to v14.18.63
  • chore(deps): update dependency @types/sharp to ^0.32.0
  • chore(deps): update dependency chai to v4.5.0
  • chore(deps): update dependency escodegen to v1.14.3
  • chore(deps): update dependency estraverse to v5.3.0
  • chore(deps): update dependency gm to v1.25.0 (gm, @types/gm)
  • chore(deps): update dependency looks-same to v7.3.0
  • chore(deps): update dependency microtime to v3.1.1
  • chore(deps): update dependency mocha to v8.4.0 (mocha, @types/mocha)
  • chore(deps): update dependency nock to v13.5.4
  • chore(deps): update dependency sinon-chai to v3.7.0
  • chore(deps): update actions/checkout action to v4
  • chore(deps): update actions/setup-node action to v4
  • chore(deps): update dependency @types/estree to v1
  • chore(deps): update dependency @types/node to v20
  • chore(deps): update dependency @types/sinon to v17
  • chore(deps): update dependency chai to v5
  • chore(deps): update dependency escope to v4
  • chore(deps): update dependency looks-same to v9
  • chore(deps): update dependency mkdirp to v3
  • chore(deps): update dependency mocha to v10 (mocha, @types/mocha)
  • chore(deps): update dependency nyc to v17
  • chore(deps): update dependency pngquant to v4
  • chore(deps): update dependency rimraf to v6
  • chore(deps): update dependency sinon to v18
  • chore(deps): update dependency sinon-chai to v4
  • chore(deps): update dependency ts-node to v10
  • chore(deps): update dependency tsc-watch to v6
  • chore(deps): update dependency tslint to v6
  • chore(deps): update dependency typescript to v5
  • ๐Ÿ” Create all rate-limited PRs at once ๐Ÿ”

Edited/Blocked

These updates have been manually edited so Renovate will no longer make changes. To discard all commits and start over, click on a checkbox.

Open

These updates have all been created already. Click a checkbox below to force a retry/rebase of any.

Detected dependencies

github-actions
.github/workflows/firebase.yml
.github/workflows/nodejs.yml
  • actions/checkout v1
  • actions/setup-node v1
.github/workflows/publish.yml
  • actions/checkout v1
  • actions/setup-node v1
  • actions/checkout v1
  • actions/setup-node v1
npm
package.json
  • buffer ^6.0.2
  • dash-ast ^2.0.0
  • ebnf ^1.9.0
  • es6-promise-pool ^2.5.0
  • escodegen ^1.12.0
  • escope ^3.6.0
  • estraverse ^5.0.0
  • gm ^1.23.1
  • microtime 3.0.0
  • moo ^0.5.1
  • pngquant ^3.1.0
  • sharp ^0.26.2
  • text-encoding ^0.7.0
  • three ^0.112.1
  • wpc-emu 0.34.5
  • @types/chai ^4.2.14
  • @types/escodegen 0.0.6
  • @types/estraverse 0.0.6
  • @types/estree 0.0.42
  • @types/gm ^1.18.9
  • @types/microtime 2.1.0
  • @types/mocha ^8.0.4
  • @types/moo 0.5.3
  • @types/nearley 2.11.1
  • @types/node ^14.14.7
  • @types/sharp ^0.26.1
  • @types/sinon ^9.0.8
  • @types/sinon-chai ^3.2.5
  • chai 4.2.0
  • cross-env 7.0.2
  • esm 3.2.25
  • looks-same 7.2.3
  • mkdirp 1.0.4
  • mocha ^8.2.1
  • nock ^13.0.5
  • node-fetch ^2.6.1
  • nyc 15.1.0
  • rimraf 3.0.2
  • sinon ^9.2.1
  • sinon-chai 3.5.0
  • ts-node 8.10.2
  • tsc-watch 4.2.8
  • tslint 5.20.1
  • typescript 3.9.5
travis
.travis.yml

  • Check this box to trigger a request for Renovate to run again on this repository

Setup Build

We should be shipping two builds:

  1. An ES2015 build that works in the browser and is tree-shakeable
  2. A CommonJS build for Node.js

Scripting minimum viable product

Scripting makes good progress, here a few things in order of priority. Attached Visual Pinball's script library.

The first mile stone is the example table script.

Here it is!
Option Explicit

'*****************************************************************************************************
' CREDITS
' Initial table created by fuzzel, jimmyfingers, jpsalas, toxie & unclewilly (in alphabetical order)
' Flipper primitives by zany
' Ball rolling sound script by jpsalas
' Ball shadow by ninuzzu
' Ball control by rothbauerw
' DOF by arngrim
' Positional sound helper functions by djrobx
' Plus a lot of input from the whole community (sorry if we forgot you :/)
'*****************************************************************************************************


'First, try to load the Controller.vbs (DOF), which helps controlling additional hardware like lights, gears, knockers, bells and chimes (to increase realism)
'This table uses DOF via the 'SoundFX' calls that are inserted in some of the PlaySound commands, which will then fire an additional event, instead of just playing a sample/sound effect
On Error Resume Next
ExecuteGlobal GetTextFile("controller.vbs")
If Err Then MsgBox "You need the Controller.vbs file in order to run this table (installed with the VPX package in the scripts folder)"
On Error Goto 0

'If using Visual PinMAME (VPM), place the ROM/game name in the constant below,
'both for VPM, and DOF to load the right DOF config from the Configtool, whether it's a VPM or an Original table

'Const cGameName = ""

If Table1.ShowDT = false then
    Scoretext.Visible = false
End If


Dim EnableBallControl
EnableBallControl = false 'Change to true to enable manual ball control (or press C in-game) via the arrow keys and B (boost movement) keys

Const BallSize = 25  'Ball radius

Sub Table1_KeyDown(ByVal keycode)
	If keycode = PlungerKey Then
		Plunger.PullBack
		PlaySound "plungerpull",0,1,AudioPan(Plunger),0.25,0,0,1,AudioFade(Plunger)
	End If

	If keycode = LeftFlipperKey Then
		LeftFlipper.RotateToEnd
		PlaySound SoundFX("fx_flipperup",DOFFlippers), 0, .67, AudioPan(LeftFlipper), 0.05,0,0,1,AudioFade(LeftFlipper)
	End If

	If keycode = RightFlipperKey Then
		RightFlipper.RotateToEnd
		PlaySound SoundFX("fx_flipperup",DOFFlippers), 0, .67, AudioPan(RightFlipper), 0.05,0,0,1,AudioFade(RightFlipper)
	End If

	If keycode = LeftTiltKey Then
		Nudge 90, 2
	End If

	If keycode = RightTiltKey Then
		Nudge 270, 2
	End If

	If keycode = CenterTiltKey Then
		Nudge 0, 2
	End If

    ' Manual Ball Control
	If keycode = 46 Then	 				' C Key
		If EnableBallControl = 1 Then
			EnableBallControl = 0
		Else
			EnableBallControl = 1
		End If
	End If
    If EnableBallControl = 1 Then
		If keycode = 48 Then 				' B Key
			If BCboost = 1 Then
				BCboost = BCboostmulti
			Else
				BCboost = 1
			End If
		End If
		If keycode = 203 Then BCleft = 1	' Left Arrow
		If keycode = 200 Then BCup = 1		' Up Arrow
		If keycode = 208 Then BCdown = 1	' Down Arrow
		If keycode = 205 Then BCright = 1	' Right Arrow
	End If
End Sub

Sub Table1_KeyUp(ByVal keycode)
	If keycode = PlungerKey Then
		Plunger.Fire
		PlaySound "plunger",0,1,AudioPan(Plunger),0.25,0,0,1,AudioFade(Plunger)
	End If

	If keycode = LeftFlipperKey Then
		LeftFlipper.RotateToStart
		PlaySound SoundFX("fx_flipperdown",DOFFlippers), 0, 1, AudioPan(LeftFlipper), 0.05,0,0,1,AudioFade(LeftFlipper)
	End If

	If keycode = RightFlipperKey Then
		RightFlipper.RotateToStart
		PlaySound SoundFX("fx_flipperdown",DOFFlippers), 0, 1, AudioPan(RightFlipper), 0.05,0,0,1,AudioFade(RightFlipper)
	End If

    'Manual Ball Control
	If EnableBallControl = 1 Then
		If keycode = 203 Then BCleft = 0	' Left Arrow
		If keycode = 200 Then BCup = 0		' Up Arrow
		If keycode = 208 Then BCdown = 0	' Down Arrow
		If keycode = 205 Then BCright = 0	' Right Arrow
	End If
End Sub

Sub playfield_mesh_Hit()
	msgbox "ball has hit playfield"
end Sub

Sub Drain_Hit()
	PlaySound "drain",0,1,AudioPan(Drain),0.25,0,0,1,AudioFade(Drain)
	Drain.DestroyBall
	BIP = BIP - 1
	If BIP = 0 then
		'Plunger.CreateBall
		BallRelease.CreateBall
		BallRelease.Kick 90, 7
		PlaySound SoundFX("ballrelease",DOFContactors), 0,1,AudioPan(BallRelease),0.25,0,0,1,AudioFade(BallRelease)
		BIP = BIP + 1
	End If
End Sub


Dim BIP
BIP = 0

Sub Plunger_Init()
	PlaySound SoundFX("ballrelease",DOFContactors), 0,1,AudioPan(BallRelease),0.25,0,0,1,AudioFade(BallRelease)
	'Plunger.CreateBall
	BallRelease.CreateBall
	BallRelease.Kick 90, 7
	BIP = BIP + 1
End Sub

Sub Gate_Hit
	Kicker1.Kick 190, 10
End Sub

Sub Bumper1_Hit
	PlaySound SoundFX("fx_bumper4",DOFContactors), 0,1,AudioPan(Bumper1),0,0,0,1,AudioFade(Bumper1)
	B1L1.State = 1:B1L2. State = 1
	Me.TimerEnabled = 1
End Sub

Sub Bumper1_Timer
	B1L1.State = 0:B1L2. State = 0
	Me.Timerenabled = 0
End Sub

Sub Bumper2_Hit
	PlaySound SoundFX("fx_bumper4",DOFContactors), 0,1,AudioPan(Bumper2),0,0,0,1,AudioFade(Bumper2)
	B2L1.State = 1:B2L2. State = 1
	Me.TimerEnabled = 1
End Sub

Sub Bumper2_Timer
	B2L1.State = 0:B2L2. State = 0
	Me.Timerenabled = 0
End Sub	

Sub Bumper3_Hit
	PlaySound SoundFX("fx_bumper4",DOFContactors), 0,1,AudioPan(Bumper3),0,0,0,1,AudioFade(Bumper3)
	B3L1.State = 1:B3L2. State = 1
	Me.TimerEnabled = 1
End Sub

Sub Bumper3_Timer
	B3L1.State = 0:B3L2. State = 0
	Me.Timerenabled = 0
End Sub

Sub Bumper4_Hit
	PlaySound SoundFX("fx_bumper4",DOFContactors), 0,1,AudioPan(Bumper4),0,0,0,1,AudioFade(Bumper4)
	B4L1.State = 1:B4L2. State = 1
	Me.TimerEnabled = 1
End Sub

Sub Bumper4_Timer
	B4L1.State = 0:B4L2. State = 0
	Me.Timerenabled = 0
End Sub

Sub Bumper5_Hit
	PlaySound SoundFX("fx_bumper4",DOFContactors), 0,1,AudioPan(Bumper5),0,0,0,1,AudioFade(Bumper5)
	B5L1.State = 1:B5L2. State = 1
	Me.TimerEnabled = 1
End Sub

Sub Bumper5_Timer
	B5L1.State = 0:B5L2. State = 0
	Me.Timerenabled = 0
End Sub

Sub sw9_Hit
	If L9.State = 1 then 
		L9.State  = 0
	else 
		L9.State = 1
	end if
End Sub

Sub sw8_Hit
	If L8.State = 1 then 
		L8.State  = 0
	else 
		L8.State = 1
	end if
End Sub

Sub sw7_Hit
	If L7.State = 1 then 
		L7.State  = 0
	else 
		L7.State = 1
	end if
End Sub

Sub sw6_Hit
	If L6.State = 1 then 
		L6.State  = 0
	else 
		L6.State = 1
	end if
End Sub


'****Targets
Sub sw1_Hit
	If L1.State = 1 then 
		L1.State  = 0
	else 
		L1.State = 1
	end if
	Me.TimerEnabled = 1
End Sub

Sub sw1_Timer
	sw1.isDropped=0
	Me.TimerEnabled = 0
End Sub

Sub sw2_Hit
	If L2.State = 1 then 
		L2.State  = 0
	else 
		L2.State = 1
	end if
	Me.TimerEnabled = 1
End Sub

Sub sw2_Timer
	sw2.isDropped=0
	Me.TimerEnabled = 0
End Sub

Sub sw3_Hit
	If L3.State = 1 then 
		L3.State  = 0
	else 
		L3.State = 1
	end if
	ME.TimerEnabled = 1
End Sub

Sub sw3_Timer
	sw3.isDropped = 0
	Me.TimerEnabled = 0
End Sub

Sub sw11_Hit
	If L11.State = 1 then 
		L11.State  = 0
	else 
		L11.State = 1
	end if
End Sub


Sub sw12_Hit
	If L12.State = 1 then 
		L12.State  = 0
	else 
		L12.State = 1
	end if
End Sub


Sub sw13_Hit
	If L13.State = 1 then 
		L13.State  = 0
	else 
		L13.State = 1
	end if
End Sub

Sub Kicker1_Hit
	PlaySound "kicker_enter_center", 0, Vol(ActiveBall), AudioPan(ActiveBall), 0, Pitch(ActiveBall), 1, 0, AudioFade(ActiveBall)
	'Plunger.CreateBall
	PlaySound SoundFX("ballrelease",DOFContactors), 0,0.5,AudioPan(BallRelease),0.25,0,0,1,AudioFade(BallRelease)
	BallRelease.CreateBall
	BallRelease.Kick 90, 8
	BIP = BIP +1
End Sub

Sub Kicker1_UnHit
	PlaySound "popper_ball",0,.75,AudioPan(Kicker1),0.25,0,0,1,AudioFade(Kicker1)
End Sub

'*****GI Lights On
dim xx

For each xx in GI:xx.State = 1: Next

'**********Sling Shot Animations
' Rstep and Lstep  are the variables that increment the animation
'****************
Dim RStep, Lstep

Sub RightSlingShot_Slingshot
    PlaySound SoundFX("right_slingshot",DOFContactors), 0,1, 0.05,0.05 '0,1, AudioPan(RightSlingShot), 0.05,0,0,1,AudioFade(RightSlingShot)
    RSling.Visible = 0
    RSling1.Visible = 1
    sling1.rotx = 20
    RStep = 0
    RightSlingShot.TimerEnabled = 1
	gi1.State = 0:Gi2.State = 0
End Sub

Sub RightSlingShot_Timer
    Select Case RStep
        Case 3:RSLing1.Visible = 0:RSLing2.Visible = 1:sling1.rotx = 10
        Case 4:RSLing2.Visible = 0:RSLing.Visible = 1:sling1.rotx = 0:RightSlingShot.TimerEnabled = 0:gi1.State = 1:Gi2.State = 1
    End Select
    RStep = RStep + 1
End Sub

Sub LeftSlingShot_Slingshot
    PlaySound SoundFX("left_slingshot",DOFContactors), 0,1, -0.05,0.05 '0,1, AudioPan(LeftSlingShot), 0.05,0,0,1,AudioFade(LeftSlingShot)
    LSling.Visible = 0
    LSling1.Visible = 1
    sling2.rotx = 20
    LStep = 0
    LeftSlingShot.TimerEnabled = 1
	gi3.State = 0:Gi4.State = 0
End Sub

Sub LeftSlingShot_Timer
    Select Case LStep
        Case 3:LSLing1.Visible = 0:LSLing2.Visible = 1:sling2.rotx = 10
        Case 4:LSLing2.Visible = 0:LSLing.Visible = 1:sling2.rotx = 0:LeftSlingShot.TimerEnabled = 0:gi3.State = 1:Gi4.State = 1
    End Select
    LStep = LStep + 1
End Sub


'*********************************************************************
'                 Positional Sound Playback Functions
'*********************************************************************

' Play a sound, depending on the X,Y position of the table element (especially cool for surround speaker setups, otherwise stereo panning only)
' parameters (defaults): loopcount (1), volume (1), randompitch (0), pitch (0), useexisting (0), restart (1))
' Note that this will not work (currently) for walls/slingshots as these do not feature a simple, single X,Y position
Sub PlayXYSound(soundname, tableobj, loopcount, volume, randompitch, pitch, useexisting, restart)
	PlaySound soundname, loopcount, volume, AudioPan(tableobj), randompitch, pitch, useexisting, restart, AudioFade(tableobj)
End Sub

' Similar subroutines that are less complicated to use (e.g. simply use standard parameters for the PlaySound call)
Sub PlaySoundAt(soundname, tableobj)
    PlaySound soundname, 1, 1, AudioPan(tableobj), 0,0,0, 1, AudioFade(tableobj)
End Sub

Sub PlaySoundAtBall(soundname)
    PlaySoundAt soundname, ActiveBall
End Sub


'*********************************************************************
'                     Supporting Ball & Sound Functions
'*********************************************************************

Function AudioFade(tableobj) ' Fades between front and back of the table (for surround systems or 2x2 speakers, etc), depending on the Y position on the table. "table1" is the name of the table
	Dim tmp
    tmp = tableobj.y * 2 / table1.height-1
    If tmp > 0 Then
		AudioFade = Csng(tmp ^10)
    Else
        AudioFade = Csng(-((- tmp) ^10) )
    End If
End Function

Function AudioPan(tableobj) ' Calculates the pan for a tableobj based on the X position on the table. "table1" is the name of the table
    Dim tmp
    tmp = tableobj.x * 2 / table1.width-1
    If tmp > 0 Then
        AudioPan = Csng(tmp ^10)
    Else
        AudioPan = Csng(-((- tmp) ^10) )
    End If
End Function

Function Vol(ball) ' Calculates the Volume of the sound based on the ball speed
    Vol = Csng(BallVel(ball) ^2 / 2000)
End Function

Function Pitch(ball) ' Calculates the pitch of the sound based on the ball speed
    Pitch = BallVel(ball) * 20
End Function

Function BallVel(ball) 'Calculates the ball speed
    BallVel = INT(SQR((ball.VelX ^2) + (ball.VelY ^2) ) )
End Function


'*****************************************
'   rothbauerw's Manual Ball Control
'*****************************************

Dim BCup, BCdown, BCleft, BCright
Dim ControlBallInPlay, ControlActiveBall
Dim BCvel, BCyveloffset, BCboostmulti, BCboost

BCboost = 1				'Do Not Change - default setting
BCvel = 4				'Controls the speed of the ball movement
BCyveloffset = -0.01 	'Offsets the force of gravity to keep the ball from drifting vertically on the table, should be negative
BCboostmulti = 3		'Boost multiplier to ball veloctiy (toggled with the B key) 

ControlBallInPlay = false

Sub StartBallControl_Hit()
	Set ControlActiveBall = ActiveBall
	ControlBallInPlay = true
End Sub

Sub StopBallControl_Hit()
	ControlBallInPlay = false
End Sub	

Sub BallControlTimer_Timer()
	If EnableBallControl and ControlBallInPlay then
		If BCright = 1 Then
			ControlActiveBall.velx =  BCvel*BCboost
		ElseIf BCleft = 1 Then
			ControlActiveBall.velx = -BCvel*BCboost
		Else
			ControlActiveBall.velx = 0
		End If

		If BCup = 1 Then
			ControlActiveBall.vely = -BCvel*BCboost
		ElseIf BCdown = 1 Then
			ControlActiveBall.vely =  BCvel*BCboost
		Else
			ControlActiveBall.vely = bcyveloffset
		End If
	End If
End Sub


'*****************************************
'      JP's VP10 Rolling Sounds
'*****************************************

Const tnob = 5 ' total number of balls
ReDim rolling(tnob)
InitRolling

Sub InitRolling
    Dim i
    For i = 0 to tnob
        rolling(i) = False
    Next
End Sub

Sub RollingTimer_Timer()
    Dim BOT, b
    BOT = GetBalls

	' stop the sound of deleted balls
    For b = UBound(BOT) + 1 to tnob
        rolling(b) = False
        StopSound("fx_ballrolling" & b)
    Next

	' exit the sub if no balls on the table
    If UBound(BOT) = -1 Then Exit Sub

	' play the rolling sound for each ball
    For b = 0 to UBound(BOT)
        If BallVel(BOT(b) ) > 1 AND BOT(b).z < 30 Then
            rolling(b) = True
            PlaySound("fx_ballrolling" & b), -1, Vol(BOT(b)), AudioPan(BOT(b)), 0, Pitch(BOT(b)), 1, 0, AudioFade(BOT(b))
        Else
            If rolling(b) = True Then
                StopSound("fx_ballrolling" & b)
                rolling(b) = False
            End If
        End If
    Next
End Sub

'**********************
' Ball Collision Sound
'**********************

Sub OnBallBallCollision(ball1, ball2, velocity)
	PlaySound("fx_collide"), 0, Csng(velocity) ^2 / 2000, AudioPan(ball1), 0, Pitch(ball1), 0, 0, AudioFade(ball1)
End Sub


'*****************************************
'	ninuzzu's	FLIPPER SHADOWS
'*****************************************

sub FlipperTimer_Timer()
	FlipperLSh.RotZ = LeftFlipper.currentangle
	FlipperRSh.RotZ = RightFlipper.currentangle

End Sub

'*****************************************
'	ninuzzu's	BALL SHADOW
'*****************************************
Dim BallShadow
BallShadow = Array (BallShadow1,BallShadow2,BallShadow3,BallShadow4,BallShadow5)

Sub BallShadowUpdate_timer()
    Dim BOT, b
    BOT = GetBalls
    ' hide shadow of deleted balls
    If UBound(BOT)<(tnob-1) Then
        For b = (UBound(BOT) + 1) to (tnob-1)
            BallShadow(b).visible = 0
        Next
    End If
    ' exit the Sub if no balls on the table
    If UBound(BOT) = -1 Then Exit Sub
    ' render the shadow for each ball
    For b = 0 to UBound(BOT)
        If BOT(b).X < Table1.Width/2 Then
            BallShadow(b).X = ((BOT(b).X) - (Ballsize/6) + ((BOT(b).X - (Table1.Width/2))/7)) + 6
        Else
            BallShadow(b).X = ((BOT(b).X) + (Ballsize/6) + ((BOT(b).X - (Table1.Width/2))/7)) - 6
        End If
        ballShadow(b).Y = BOT(b).Y + 12
        If BOT(b).Z > 20 Then
            BallShadow(b).visible = 1
        Else
            BallShadow(b).visible = 0
        End If
    Next
End Sub



'************************************
' What you need to add to your table
'************************************

' a timer called RollingTimer. With a fast interval, like 10
' one collision sound, in this script is called fx_collide
' as many sound files as max number of balls, with names ending with 0, 1, 2, 3, etc
' for ex. as used in this script: fx_ballrolling0, fx_ballrolling1, fx_ballrolling2, fx_ballrolling3, etc


'******************************************
' Explanation of the rolling sound routine
'******************************************

' sounds are played based on the ball speed and position

' the routine checks first for deleted balls and stops the rolling sound.

' The For loop goes through all the balls on the table and checks for the ball speed and 
' if the ball is on the table (height lower than 30) then then it plays the sound
' otherwise the sound is stopped, like when the ball has stopped or is on a ramp or flying.

' The sound is played using the VOL, AUDIOPAN, AUDIOFADE and PITCH functions, so the volume and pitch of the sound
' will change according to the ball speed, and the AUDIOPAN & AUDIOFADE functions will change the stereo position
' according to the position of the ball on the table.


'**************************************
' Explanation of the collision routine
'**************************************

' The collision is built in VP.
' You only need to add a Sub OnBallBallCollision(ball1, ball2, velocity) and when two balls collide they 
' will call this routine. What you add in the sub is up to you. As an example is a simple Playsound with volume and paning
' depending of the speed of the collision.


Sub Pins_Hit (idx)
	PlaySound "pinhit_low", 0, Vol(ActiveBall), AudioPan(ActiveBall), 0, Pitch(ActiveBall), 0, 0, AudioFade(ActiveBall)
End Sub

Sub Targets_Hit (idx)
	PlaySound "target", 0, Vol(ActiveBall), AudioPan(ActiveBall), 0, Pitch(ActiveBall), 0, 0, AudioFade(ActiveBall)
End Sub

Sub Metals_Thin_Hit (idx)
	PlaySound "metalhit_thin", 0, Vol(ActiveBall), AudioPan(ActiveBall), 0, Pitch(ActiveBall), 1, 0, AudioFade(ActiveBall)
End Sub

Sub Metals_Medium_Hit (idx)
	PlaySound "metalhit_medium", 0, Vol(ActiveBall), AudioPan(ActiveBall), 0, Pitch(ActiveBall), 1, 0, AudioFade(ActiveBall)
End Sub

Sub Metals2_Hit (idx)
	PlaySound "metalhit2", 0, Vol(ActiveBall), AudioPan(ActiveBall), 0, Pitch(ActiveBall), 1, 0, AudioFade(ActiveBall)
End Sub

Sub Gates_Hit (idx)
	PlaySound "gate4", 0, Vol(ActiveBall), AudioPan(ActiveBall), 0, Pitch(ActiveBall), 1, 0, AudioFade(ActiveBall)
End Sub

Sub Spinner_Spin
	PlaySound "fx_spinner", 0, .25, AudioPan(Spinner), 0.25, 0, 0, 1, AudioFade(Spinner)
End Sub

Sub Rubbers_Hit(idx)
 	dim finalspeed
  	finalspeed=SQR(activeball.velx * activeball.velx + activeball.vely * activeball.vely)
 	If finalspeed > 20 then 
		PlaySound "fx_rubber2", 0, Vol(ActiveBall), AudioPan(ActiveBall), 0, Pitch(ActiveBall), 1, 0, AudioFade(ActiveBall)
	End if
	If finalspeed >= 6 AND finalspeed <= 20 then
 		RandomSoundRubber()
 	End If
End Sub

Sub Posts_Hit(idx)
 	dim finalspeed
  	finalspeed=SQR(activeball.velx * activeball.velx + activeball.vely * activeball.vely)
 	If finalspeed > 16 then 
		PlaySound "fx_rubber2", 0, Vol(ActiveBall), AudioPan(ActiveBall), 0, Pitch(ActiveBall), 1, 0, AudioFade(ActiveBall)
	End if
	If finalspeed >= 6 AND finalspeed <= 16 then
 		RandomSoundRubber()
 	End If
End Sub

Sub RandomSoundRubber()
	Select Case Int(Rnd*3)+1
		Case 1 : PlaySound "rubber_hit_1", 0, Vol(ActiveBall), AudioPan(ActiveBall), 0, Pitch(ActiveBall), 1, 0, AudioFade(ActiveBall)
		Case 2 : PlaySound "rubber_hit_2", 0, Vol(ActiveBall), AudioPan(ActiveBall), 0, Pitch(ActiveBall), 1, 0, AudioFade(ActiveBall)
		Case 3 : PlaySound "rubber_hit_3", 0, Vol(ActiveBall), AudioPan(ActiveBall), 0, Pitch(ActiveBall), 1, 0, AudioFade(ActiveBall)
	End Select
End Sub

Sub LeftFlipper_Collide(parm)
 	RandomSoundFlipper()
End Sub

Sub RightFlipper_Collide(parm)
 	RandomSoundFlipper()
End Sub

Sub RandomSoundFlipper()
	Select Case Int(Rnd*3)+1
		Case 1 : PlaySound "flip_hit_1", 0, Vol(ActiveBall), AudioPan(ActiveBall), 0, Pitch(ActiveBall), 1, 0, AudioFade(ActiveBall)
		Case 2 : PlaySound "flip_hit_2", 0, Vol(ActiveBall), AudioPan(ActiveBall), 0, Pitch(ActiveBall), 1, 0, AudioFade(ActiveBall)
		Case 3 : PlaySound "flip_hit_3", 0, Vol(ActiveBall), AudioPan(ActiveBall), 0, Pitch(ActiveBall), 1, 0, AudioFade(ActiveBall)
	End Select
End Sub

As far as I can see (@jsm174 please update if necessary), these are the missing pieces:

  • Multiple statements on one line
  • Function declarations
  • Switch statements
  • Array handling (declaration, access)
  • Pass by reference and value (ByRef & ByVal)
  • Error handling

The next step would then be to compile controller.vbs, core.vbs, and the ROM controller scripts. Lastly, the table scripts.

Links

Implement Audio (Table provided mechanical audio)

Audio is currently completely absent, even on parse level, i.e. the audio streams are skipped. What needs to be done:

  • Parse the audio data from the VPX file
  • Make them accessible by name in the table object
  • Add a JavaScript API to play them, with the parameters Visual Pinball provides, namely:
    • loop count
    • volume
    • pan,
    • random pitch
    • pitch,
    • use same (?)
    • restart
    • front rear fade

We might be able to stream the audio directly from the table blob. To check. A good high-level audio library seems to be Howler.

Implement Nudging

Mainly port the nudging part of the physics loop. There are a few TODO markers.

Add support for Babylon.js

Now the renderer is abstract, we can easily replace three.js with other WebGL frameworks. Bablyon.js looks awesome, is very well documented and has a nice community.

Handle environment maps

AFAIK there are only two env maps: The global one and another one for the ball.

Loading HDR files should be okay, but now they need to be applied and possibly converted from cube maps to equirectangular.

Move function statements to the top

Since functions are put into the global scope object, they become expressions and not declarations and thus are only executable after definition.

Running through the top body and hoisting all functions to the top should solve this problem.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.